@things-factory/auth-base 10.1.106 → 10.1.111

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-server/ai-entry/access-assistant.d.ts +50 -0
  2. package/dist-server/ai-entry/access-assistant.js +145 -0
  3. package/dist-server/ai-entry/access-assistant.js.map +1 -0
  4. package/dist-server/ai-entry/access-entry-targets.d.ts +32 -0
  5. package/dist-server/ai-entry/access-entry-targets.js +197 -0
  6. package/dist-server/ai-entry/access-entry-targets.js.map +1 -0
  7. package/dist-server/ai-entry/entry-target-shape.d.ts +82 -0
  8. package/dist-server/ai-entry/entry-target-shape.js +3 -0
  9. package/dist-server/ai-entry/entry-target-shape.js.map +1 -0
  10. package/dist-server/controllers/access-request.d.ts +6 -0
  11. package/dist-server/controllers/access-request.js +104 -26
  12. package/dist-server/controllers/access-request.js.map +1 -1
  13. package/dist-server/index.d.ts +3 -0
  14. package/dist-server/index.js +3 -0
  15. package/dist-server/index.js.map +1 -1
  16. package/dist-server/service/access-request/access-request-query.d.ts +7 -0
  17. package/dist-server/service/access-request/access-request-query.js +37 -5
  18. package/dist-server/service/access-request/access-request-query.js.map +1 -1
  19. package/dist-server/service/index.d.ts +1 -1
  20. package/dist-server/service/open-fields.js +2 -0
  21. package/dist-server/service/open-fields.js.map +1 -1
  22. package/dist-server/service/privilege/privilege-directive.js +2 -2
  23. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  24. package/dist-server/service/privilege/privilege-rejection.d.ts +3 -1
  25. package/dist-server/service/privilege/privilege-rejection.js +2 -10
  26. package/dist-server/service/privilege/privilege-rejection.js.map +1 -1
  27. package/dist-server/service/role/role-query.d.ts +2 -0
  28. package/dist-server/service/role/role-query.js +12 -0
  29. package/dist-server/service/role/role-query.js.map +1 -1
  30. package/dist-server/service/user/index.d.ts +1 -1
  31. package/dist-server/service/user/user-mutation.js +1 -1
  32. package/dist-server/service/user/user-mutation.js.map +1 -1
  33. package/dist-server/tsconfig.tsbuildinfo +1 -1
  34. package/dist-server/utils/access-request-review.d.ts +3 -0
  35. package/dist-server/utils/access-request-review.js +3 -0
  36. package/dist-server/utils/access-request-review.js.map +1 -1
  37. package/dist-server/utils/assignable-roles.d.ts +11 -1
  38. package/dist-server/utils/assignable-roles.js +73 -1
  39. package/dist-server/utils/assignable-roles.js.map +1 -1
  40. package/package.json +2 -2
  41. package/tests/access-assistant-db.test.ts +155 -0
  42. package/tests/access-entry-targets-db.test.ts +138 -0
  43. package/tests/access-request-db.test.ts +84 -3
  44. package/tests/access-request-pg.test.ts +83 -0
  45. package/tests/access-request-review.test.ts +8 -0
  46. package/tests/assignable-roles-db.test.ts +79 -1
  47. package/tests/privilege-directive.test.ts +18 -0
  48. package/translations/en.json +4 -1
  49. package/translations/ja.json +4 -1
  50. package/translations/ko.json +4 -1
  51. package/translations/ms.json +4 -1
  52. package/translations/zh.json +4 -1
@@ -0,0 +1,50 @@
1
+ export declare const AUTH_ACCESS_TOOLS = "auth-access";
2
+ export declare const AUTH_ACCESS_MODE = "auth.access";
3
+ /** The subject the access request list stands for - the application sets its surface to this on that screen. */
4
+ export declare const ACCESS_REQUESTS_SUBJECT = "access-requests";
5
+ /** The screen an application recognises (auth-ui's page tag and route). */
6
+ export declare const ACCESS_REQUESTS_PAGE: {
7
+ tagname: string;
8
+ route: string;
9
+ };
10
+ type ReadSpec = {
11
+ kind: 'read';
12
+ name: string;
13
+ description: string;
14
+ doors: string[];
15
+ schema: any;
16
+ builder: (args: any, context: {
17
+ state?: any;
18
+ }) => any | Promise<any>;
19
+ };
20
+ export type AccessToolCategory = {
21
+ name: string;
22
+ description?: string;
23
+ guidance: string;
24
+ specs: ReadSpec[];
25
+ };
26
+ /**
27
+ * The pending requests the caller may decide, each with the roles that would grant it, smallest first.
28
+ * Refuses a caller who may not decide - the gate in front of it says the same, and this says it again on the server.
29
+ */
30
+ export declare function pendingRequestsToDecide(state: any): Promise<{
31
+ refused: string;
32
+ requests?: undefined;
33
+ } | {
34
+ requests: any[];
35
+ refused?: undefined;
36
+ }>;
37
+ export declare function accessToolCategory(): AccessToolCategory;
38
+ /** The mode an application registers for the access request list. Words are auth-ui's. */
39
+ export declare function accessAssistantMode(): {
40
+ kind: string;
41
+ subjects: string[];
42
+ defaultFor: string[];
43
+ labelKey: string;
44
+ systemPrompt: string;
45
+ toolCategories: string[];
46
+ entryTargets: string[];
47
+ requireToolOnFirstTurn: boolean;
48
+ placeholderKey: string;
49
+ };
50
+ export {};
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ACCESS_REQUESTS_PAGE = exports.ACCESS_REQUESTS_SUBJECT = exports.AUTH_ACCESS_MODE = exports.AUTH_ACCESS_TOOLS = void 0;
37
+ exports.pendingRequestsToDecide = pendingRequestsToDecide;
38
+ exports.accessToolCategory = accessToolCategory;
39
+ exports.accessAssistantMode = accessAssistantMode;
40
+ /*
41
+ * What the assistant reads to draft access decisions, and the mode an application registers (ADR-0103 decision 6).
42
+ *
43
+ * ── Why a read tool and not the screen's context ─────────────────────────
44
+ * The screen's `hostContext` goes to the tools (`toolContext.host`), never into the model's prompt
45
+ * (ai-client-base registry-assistant.ts: the prompt is `systemPrompt + guidance`). Pending requests put there
46
+ * would be invisible to the model. So the model reads them through this tool, run on the server as the caller:
47
+ * this domain only, only for someone who may decide, and never the caller's own requests.
48
+ *
49
+ * ── Plain data ────────────────────────────────────────────────────────────
50
+ * Like the entry targets: no dependency on the assistant. An application that runs it registers the category,
51
+ * the mode and the two targets. One that does not register them has no tool - whether colleagues' names and
52
+ * reasons go to an external model is that product's decision.
53
+ */
54
+ const access_entry_targets_js_1 = require("./access-entry-targets.js");
55
+ exports.AUTH_ACCESS_TOOLS = 'auth-access';
56
+ exports.AUTH_ACCESS_MODE = 'auth.access';
57
+ /** The subject the access request list stands for - the application sets its surface to this on that screen. */
58
+ exports.ACCESS_REQUESTS_SUBJECT = 'access-requests';
59
+ /** The screen an application recognises (auth-ui's page tag and route). */
60
+ exports.ACCESS_REQUESTS_PAGE = { tagname: 'access-requests-page', route: 'access-requests' };
61
+ /** The declaration the decision door carries - who may read pending requests to decide them. */
62
+ const DECIDER = { category: 'user', privilege: 'mutation', owner: true };
63
+ /**
64
+ * The pending requests the caller may decide, each with the roles that would grant it, smallest first.
65
+ * Refuses a caller who may not decide - the gate in front of it says the same, and this says it again on the server.
66
+ */
67
+ async function pendingRequestsToDecide(state) {
68
+ const [{ judgePermission }, access, { getRepository }, { AccessRequest, AccessRequestStatus }, { effectiveState }] = await Promise.all([
69
+ Promise.resolve().then(() => __importStar(require('../utils/check-permission.js'))),
70
+ Promise.resolve().then(() => __importStar(require('../controllers/access-request.js'))),
71
+ Promise.resolve().then(() => __importStar(require('@things-factory/shell'))),
72
+ Promise.resolve().then(() => __importStar(require('../service/access-request/access-request.js'))),
73
+ Promise.resolve().then(() => __importStar(require('../utils/access-request-review.js')))
74
+ ]);
75
+ const { domain, user } = state || {};
76
+ if (!domain?.id || !user?.id)
77
+ return { refused: 'no-domain' };
78
+ const judgement = await judgePermission(DECIDER, user, domain, state.unsafeIP, state.prohibitedPrivileges);
79
+ if (judgement.permitted !== true)
80
+ return { refused: 'not-a-decider' };
81
+ const now = new Date();
82
+ const rows = await getRepository(AccessRequest).find({
83
+ where: { domain: { id: domain.id }, state: AccessRequestStatus.PENDING },
84
+ relations: ['requester'],
85
+ order: { createdAt: 'ASC' },
86
+ take: 200
87
+ });
88
+ const requests = [];
89
+ for (const row of rows) {
90
+ if (row.requester?.id === user.id)
91
+ continue;
92
+ if (effectiveState(row.state, row.expiresAt, now) !== 'PENDING')
93
+ continue;
94
+ const candidates = await access.rolesGranting(row.category, row.privilege, domain);
95
+ requests.push({
96
+ request: `${row.requester?.name || row.requester?.email || ''} · ${row.category}:${row.privilege}`,
97
+ requester: row.requester?.name || row.requester?.email || '',
98
+ privilege: `${row.category}:${row.privilege}`,
99
+ refusedAt: row.operationName || null,
100
+ reason: row.reason || null,
101
+ timesAsked: row.count,
102
+ askedAt: row.createdAt,
103
+ roles: candidates.map(({ name, privilegeCount }) => ({ name, privilegeCount }))
104
+ });
105
+ }
106
+ return { requests };
107
+ }
108
+ function accessToolCategory() {
109
+ return {
110
+ name: exports.AUTH_ACCESS_TOOLS,
111
+ description: 'access requests waiting for a decision in this domain',
112
+ guidance: [
113
+ 'To draft decisions on access requests, call listPendingAccessRequests first and draft only from what it returns.',
114
+ 'Propose GRANT with the role that has the fewest privileges among the roles listed for that request; name another role only when the person asks.',
115
+ 'Never write a reason to deny - ask the person for it. Never say a request was granted or denied: the person presses the card.',
116
+ 'If a request lists no roles, say no role in this domain grants it and that a role must hold it first; do not propose it.'
117
+ ].join('\n'),
118
+ specs: [
119
+ {
120
+ kind: 'read',
121
+ name: 'listPendingAccessRequests',
122
+ description: 'Lists the access requests in this domain that are waiting for a decision the caller may make, each with the roles that would grant it (fewest privileges first).',
123
+ /* Reading requests and their candidate roles - both doors; the second carries the deciding declaration. */
124
+ doors: ['Query.accessRequests', 'Query.rolesGranting'],
125
+ schema: { type: 'object', properties: {} },
126
+ builder: (_args, context) => pendingRequestsToDecide(context?.state)
127
+ }
128
+ ]
129
+ };
130
+ }
131
+ /** The mode an application registers for the access request list. Words are auth-ui's. */
132
+ function accessAssistantMode() {
133
+ return {
134
+ kind: exports.AUTH_ACCESS_MODE,
135
+ subjects: [exports.ACCESS_REQUESTS_SUBJECT],
136
+ defaultFor: [exports.ACCESS_REQUESTS_SUBJECT],
137
+ labelKey: 'access-request.dock.mode',
138
+ systemPrompt: 'You help the person deciding access requests in this domain. You draft decisions as a card the person reviews and presses; the server checks each decision and the card shows what it found.',
139
+ toolCategories: [exports.AUTH_ACCESS_TOOLS],
140
+ entryTargets: [access_entry_targets_js_1.AUTH_ACCESS_DECISION],
141
+ requireToolOnFirstTurn: true,
142
+ placeholderKey: 'access-request.dock.placeholder'
143
+ };
144
+ }
145
+ //# sourceMappingURL=access-assistant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-assistant.js","sourceRoot":"","sources":["../../server/ai-entry/access-assistant.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the assistant reads to draft access decisions, and the mode an application registers (ADR-0103 decision 6).\n *\n * ── Why a read tool and not the screen's context ─────────────────────────\n * The screen's `hostContext` goes to the tools (`toolContext.host`), never into the model's prompt\n * (ai-client-base registry-assistant.ts: the prompt is `systemPrompt + guidance`). Pending requests put there\n * would be invisible to the model. So the model reads them through this tool, run on the server as the caller:\n * this domain only, only for someone who may decide, and never the caller's own requests.\n *\n * ── Plain data ────────────────────────────────────────────────────────────\n * Like the entry targets: no dependency on the assistant. An application that runs it registers the category,\n * the mode and the two targets. One that does not register them has no tool - whether colleagues' names and\n * reasons go to an external model is that product's decision.\n */\nimport { AUTH_ACCESS_DECISION } from './access-entry-targets.js'\n\nexport const AUTH_ACCESS_TOOLS = 'auth-access'\nexport const AUTH_ACCESS_MODE = 'auth.access'\n/** The subject the access request list stands for - the application sets its surface to this on that screen. */\nexport const ACCESS_REQUESTS_SUBJECT = 'access-requests'\n/** The screen an application recognises (auth-ui's page tag and route). */\nexport const ACCESS_REQUESTS_PAGE = { tagname: 'access-requests-page', route: 'access-requests' }\n\n/** The declaration the decision door carries - who may read pending requests to decide them. */\nconst DECIDER = { category: 'user', privilege: 'mutation', owner: true }\n\ntype ReadSpec = {\n kind: 'read'\n name: string\n description: string\n doors: string[]\n schema: any\n builder: (args: any, context: { state?: any }) => any | Promise<any>\n}\n\nexport type AccessToolCategory = { name: string; description?: string; guidance: string; specs: ReadSpec[] }\n\n/**\n * The pending requests the caller may decide, each with the roles that would grant it, smallest first.\n * Refuses a caller who may not decide - the gate in front of it says the same, and this says it again on the server.\n */\nexport async function pendingRequestsToDecide(state: any) {\n const [{ judgePermission }, access, { getRepository }, { AccessRequest, AccessRequestStatus }, { effectiveState }] = await Promise.all([\n import('../utils/check-permission.js'),\n import('../controllers/access-request.js'),\n import('@things-factory/shell'),\n import('../service/access-request/access-request.js'),\n import('../utils/access-request-review.js')\n ])\n const { domain, user } = state || {}\n if (!domain?.id || !user?.id) return { refused: 'no-domain' }\n\n const judgement = await judgePermission(DECIDER, user, domain, state.unsafeIP, state.prohibitedPrivileges)\n if (judgement.permitted !== true) return { refused: 'not-a-decider' }\n\n const now = new Date()\n const rows = await getRepository(AccessRequest).find({\n where: { domain: { id: domain.id }, state: AccessRequestStatus.PENDING },\n relations: ['requester'],\n order: { createdAt: 'ASC' },\n take: 200\n })\n\n const requests = []\n for (const row of rows) {\n if (row.requester?.id === user.id) continue\n if (effectiveState(row.state as any, row.expiresAt, now) !== 'PENDING') continue\n const candidates = await access.rolesGranting(row.category, row.privilege, domain)\n requests.push({\n request: `${row.requester?.name || row.requester?.email || ''} · ${row.category}:${row.privilege}`,\n requester: row.requester?.name || row.requester?.email || '',\n privilege: `${row.category}:${row.privilege}`,\n refusedAt: row.operationName || null,\n reason: row.reason || null,\n timesAsked: row.count,\n askedAt: row.createdAt,\n roles: candidates.map(({ name, privilegeCount }) => ({ name, privilegeCount }))\n })\n }\n return { requests }\n}\n\nexport function accessToolCategory(): AccessToolCategory {\n return {\n name: AUTH_ACCESS_TOOLS,\n description: 'access requests waiting for a decision in this domain',\n guidance: [\n 'To draft decisions on access requests, call listPendingAccessRequests first and draft only from what it returns.',\n 'Propose GRANT with the role that has the fewest privileges among the roles listed for that request; name another role only when the person asks.',\n 'Never write a reason to deny - ask the person for it. Never say a request was granted or denied: the person presses the card.',\n 'If a request lists no roles, say no role in this domain grants it and that a role must hold it first; do not propose it.'\n ].join('\\n'),\n specs: [\n {\n kind: 'read',\n name: 'listPendingAccessRequests',\n description:\n 'Lists the access requests in this domain that are waiting for a decision the caller may make, each with the roles that would grant it (fewest privileges first).',\n /* Reading requests and their candidate roles - both doors; the second carries the deciding declaration. */\n doors: ['Query.accessRequests', 'Query.rolesGranting'],\n schema: { type: 'object', properties: {} },\n builder: (_args, context) => pendingRequestsToDecide(context?.state)\n }\n ]\n }\n}\n\n/** The mode an application registers for the access request list. Words are auth-ui's. */\nexport function accessAssistantMode() {\n return {\n kind: AUTH_ACCESS_MODE,\n subjects: [ACCESS_REQUESTS_SUBJECT],\n defaultFor: [ACCESS_REQUESTS_SUBJECT],\n labelKey: 'access-request.dock.mode',\n systemPrompt:\n 'You help the person deciding access requests in this domain. You draft decisions as a card the person reviews and presses; the server checks each decision and the card shows what it found.',\n toolCategories: [AUTH_ACCESS_TOOLS],\n entryTargets: [AUTH_ACCESS_DECISION],\n requireToolOnFirstTurn: true,\n placeholderKey: 'access-request.dock.placeholder'\n }\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import type { EntryIssue, EntryRow, EntryTargetDeclaration } from './entry-target-shape';
2
+ export declare const AUTH_ACCESS_DECISION = "auth.access-decision";
3
+ export declare const AUTH_ACCESS_REQUEST = "auth.access-request";
4
+ /** "김○○ · warehouse:mutation" - how a request is named on the card. */
5
+ export declare function requestLabel(request: any): string;
6
+ /** The pending requests the said text names - by the requester's name or e-mail, or the privilege. Several stay several. */
7
+ export declare function requestsNamed(items: any[], said: string): {
8
+ id: string;
9
+ label: string;
10
+ }[];
11
+ /** A role named exactly - a near name is not taken. */
12
+ export declare function rolesNamed(items: any[], said: string): {
13
+ id: string;
14
+ label: string;
15
+ }[];
16
+ /** A row as the decision door takes it. */
17
+ export declare function decisionOf(row: EntryRow): {
18
+ requestId: any;
19
+ decision: string;
20
+ roleId: any;
21
+ reason: string;
22
+ };
23
+ /** The review's findings as the card's issues - the words are the screen's (`access-request.issue.<code>`). */
24
+ export declare function issuesOf(findings: {
25
+ code: string;
26
+ blocking: boolean;
27
+ field?: string;
28
+ params?: Record<string, unknown>;
29
+ }[]): EntryIssue[];
30
+ export declare function accessDecisionEntryTarget(): EntryTargetDeclaration;
31
+ export declare function accessRequestEntryTarget(): EntryTargetDeclaration;
32
+ export declare function accessEntryTargets(): EntryTargetDeclaration[];
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AUTH_ACCESS_REQUEST = exports.AUTH_ACCESS_DECISION = void 0;
37
+ exports.requestLabel = requestLabel;
38
+ exports.requestsNamed = requestsNamed;
39
+ exports.rolesNamed = rolesNamed;
40
+ exports.decisionOf = decisionOf;
41
+ exports.issuesOf = issuesOf;
42
+ exports.accessDecisionEntryTarget = accessDecisionEntryTarget;
43
+ exports.accessRequestEntryTarget = accessRequestEntryTarget;
44
+ exports.accessEntryTargets = accessEntryTargets;
45
+ exports.AUTH_ACCESS_DECISION = 'auth.access-decision';
46
+ exports.AUTH_ACCESS_REQUEST = 'auth.access-request';
47
+ const PENDING_REQUESTS = 'query ($filters: [Filter!], $pagination: Pagination) { accessRequests(filters: $filters, pagination: $pagination) { items { id category privilege state requester { name email } } } }';
48
+ /* The decision's own scope - own, parent and granted roles - and the deciding declaration, not the role list's. */
49
+ const ROLES_NAMED = 'query ($name: String!) { assignableRolesNamed(name: $name) { id name } }';
50
+ /** "김○○ · warehouse:mutation" - how a request is named on the card. */
51
+ function requestLabel(request) {
52
+ const who = request?.requester?.name || request?.requester?.email || '';
53
+ return [who, request?.category && request?.privilege ? `${request.category}:${request.privilege}` : ''].filter(Boolean).join(' · ');
54
+ }
55
+ /** The pending requests the said text names - by the requester's name or e-mail, or the privilege. Several stay several. */
56
+ function requestsNamed(items, said) {
57
+ const text = (said || '').trim().toLowerCase();
58
+ if (!text)
59
+ return [];
60
+ return (items || [])
61
+ .filter(item => item?.state === 'PENDING')
62
+ .filter(item => {
63
+ if (item.id === said.trim())
64
+ return true;
65
+ const names = [item.requester?.name, item.requester?.email, `${item.category}:${item.privilege}`, requestLabel(item)];
66
+ return names.some(name => typeof name === 'string' && name.toLowerCase().includes(text));
67
+ })
68
+ .map(item => ({ id: item.id, label: requestLabel(item) }));
69
+ }
70
+ /** A role named exactly - a near name is not taken. */
71
+ function rolesNamed(items, said) {
72
+ const text = (said || '').trim().toLowerCase();
73
+ return (items || []).filter(item => typeof item?.name === 'string' && item.name.toLowerCase() === text).map(item => ({ id: item.id, label: item.name }));
74
+ }
75
+ const DECISION_FIELDS = [
76
+ {
77
+ name: 'request',
78
+ type: 'reference',
79
+ required: true,
80
+ labelKey: 'access-request.field.request',
81
+ description: "the pending access request, by the requester's name and the privilege asked for",
82
+ reference: {
83
+ query: PENDING_REQUESTS,
84
+ variables: () => ({ filters: [{ name: 'state', operator: 'eq', value: 'PENDING' }], pagination: { page: 1, limit: 500 } }),
85
+ recordsOf: (data, said) => requestsNamed(data?.accessRequests?.items ?? [], said)
86
+ }
87
+ },
88
+ {
89
+ name: 'decision',
90
+ type: 'enum',
91
+ required: true,
92
+ values: ['GRANT', 'DENY'],
93
+ labelKey: 'access-request.field.decision',
94
+ valueKeyPrefix: 'access-request.decision.',
95
+ description: 'GRANT puts the requester into a role; DENY refuses with a reason'
96
+ },
97
+ {
98
+ name: 'role',
99
+ type: 'reference',
100
+ labelKey: 'access-request.field.granted role',
101
+ description: 'the role to put the requester into, by its exact name - only for GRANT. Prefer the role with the fewest privileges that still holds the one asked for; the review names a smaller one when there is.',
102
+ reference: {
103
+ query: ROLES_NAMED,
104
+ variables: (said) => ({ name: said.trim() }),
105
+ recordsOf: (data, said) => rolesNamed(data?.assignableRolesNamed ?? [], said)
106
+ }
107
+ },
108
+ {
109
+ name: 'reason',
110
+ type: 'text',
111
+ labelKey: 'access-request.field.decision reason',
112
+ description: 'why - required for DENY. Never written by the model: ask the person.'
113
+ }
114
+ ];
115
+ const idOf = (value) => (value && typeof value === 'object' && typeof value.id === 'string' ? value.id : undefined);
116
+ /** A row as the decision door takes it. */
117
+ function decisionOf(row) {
118
+ const decision = row.values.decision === 'DENY' ? 'DENY' : 'GRANT';
119
+ return {
120
+ requestId: idOf(row.values.request),
121
+ decision,
122
+ roleId: decision === 'GRANT' ? idOf(row.values.role) : undefined,
123
+ reason: typeof row.values.reason === 'string' ? row.values.reason : undefined
124
+ };
125
+ }
126
+ /** The review's findings as the card's issues - the words are the screen's (`access-request.issue.<code>`). */
127
+ function issuesOf(findings) {
128
+ return findings.map(finding => ({
129
+ ...(finding.field && finding.field !== 'decision' ? { field: finding.field } : {}),
130
+ code: finding.code,
131
+ messageKey: `access-request.issue.${finding.code}`,
132
+ params: finding.params,
133
+ blocking: finding.blocking
134
+ }));
135
+ }
136
+ function accessDecisionEntryTarget() {
137
+ return {
138
+ key: exports.AUTH_ACCESS_DECISION,
139
+ labelKey: 'access-request.title.decision draft',
140
+ description: 'one decision on a pending access request: which request, grant or deny, the role to grant with, and the reason to deny.',
141
+ mutation: 'mutation ($decision: AccessDecisionInput!) { decideAccessRequest(decision: $decision) { id state } }',
142
+ fields: DECISION_FIELDS,
143
+ maxRows: 50,
144
+ variables: row => ({ decision: decisionOf(row) }),
145
+ appliedOf: (data, row) => ({ id: data?.decideAccessRequest?.id, label: row.values.request?.label }),
146
+ check: async (rows, { state }) => {
147
+ const { reviewDecision } = await Promise.resolve().then(() => __importStar(require('../controllers/access-request.js')));
148
+ const out = [];
149
+ for (const row of rows) {
150
+ const decision = decisionOf(row);
151
+ out.push(decision.requestId ? issuesOf(await reviewDecision(decision, { state })) : []);
152
+ }
153
+ return out;
154
+ }
155
+ };
156
+ }
157
+ const REQUEST_FIELDS = [
158
+ { name: 'category', type: 'string', required: true, labelKey: 'access-request.field.category', description: 'the category of the privilege, as the refusal named it' },
159
+ { name: 'privilege', type: 'string', required: true, labelKey: 'access-request.field.privilege', description: 'the privilege, as the refusal named it - query, mutation, ...' },
160
+ { name: 'reason', type: 'text', required: true, labelKey: 'access-request.field.reason', description: "why the person needs it, in the person's words - ask them; never write it yourself" }
161
+ ];
162
+ function accessRequestEntryTarget() {
163
+ return {
164
+ key: exports.AUTH_ACCESS_REQUEST,
165
+ labelKey: 'access-request.title.request draft',
166
+ description: 'one request for a privilege the person was refused for want of a role.',
167
+ mutation: 'mutation ($request: NewAccessRequest!) { requestAccess(request: $request) { id count } }',
168
+ fields: REQUEST_FIELDS,
169
+ maxRows: 10,
170
+ variables: row => ({ request: { category: row.values.category, privilege: row.values.privilege, reason: row.values.reason } }),
171
+ appliedOf: data => ({ id: data?.requestAccess?.id }),
172
+ /* What the door would refuse - asked before the card, so the person is not offered a request that cannot stand. */
173
+ check: async (rows, { state }) => {
174
+ const { judgePermission } = await Promise.resolve().then(() => __importStar(require('../utils/check-permission.js')));
175
+ const out = [];
176
+ for (const row of rows) {
177
+ const category = String(row.values.category ?? '');
178
+ const privilege = String(row.values.privilege ?? '');
179
+ if (!category || !privilege) {
180
+ out.push([]);
181
+ continue;
182
+ }
183
+ const judgement = await judgePermission({ category, privilege }, state?.user, state?.domain, state?.unsafeIP, state?.prohibitedPrivileges);
184
+ out.push(judgement.permitted === true
185
+ ? [{ code: 'already-holds', messageKey: 'access-request.issue.already-holds', blocking: true }]
186
+ : judgement.because !== 'role'
187
+ ? [{ code: 'not-by-role', messageKey: 'access-request.issue.not-by-role', params: { because: judgement.because }, blocking: true }]
188
+ : []);
189
+ }
190
+ return out;
191
+ }
192
+ };
193
+ }
194
+ function accessEntryTargets() {
195
+ return [accessDecisionEntryTarget(), accessRequestEntryTarget()];
196
+ }
197
+ //# sourceMappingURL=access-entry-targets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-entry-targets.js","sourceRoot":"","sources":["../../server/ai-entry/access-entry-targets.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * AI 입력 targets of the access request (ADR-0103 decision 4, docs/design/access-request.md §6).\n *\n * Plain declarations: this package does not depend on @things-factory/ai-assistant. An application that runs\n * the assistant passes them to `registerEntryTarget` and names their keys in a mode.\n *\n * ── The model drafts; a person decides ───────────────────────────────────\n * The model is given a tool that proposes a card. It is never given `decideAccessRequest`. The card's button\n * is pressed by the person, and the assistant then runs the mutation with that person's request state - the\n * door's `@privilege` and the review inside it apply as they do on the list screen.\n *\n * ── One review ────────────────────────────────────────────────────────────\n * `check` is `reviewDecision` - the function the list screen asks (`reviewAccessDecision`) and the decision\n * door runs again. The card cannot say something the screen does not.\n *\n * ⚠ The rows carry colleagues' names and the reasons they wrote, and proposing sends them to an external model.\n * Which product opens these targets is that product's decision.\n */\nimport type { EntryField, EntryIssue, EntryRow, EntryTargetDeclaration } from './entry-target-shape'\n\nexport const AUTH_ACCESS_DECISION = 'auth.access-decision'\nexport const AUTH_ACCESS_REQUEST = 'auth.access-request'\n\nconst PENDING_REQUESTS =\n 'query ($filters: [Filter!], $pagination: Pagination) { accessRequests(filters: $filters, pagination: $pagination) { items { id category privilege state requester { name email } } } }'\n\n/* The decision's own scope - own, parent and granted roles - and the deciding declaration, not the role list's. */\nconst ROLES_NAMED = 'query ($name: String!) { assignableRolesNamed(name: $name) { id name } }'\n\n/** \"김○○ · warehouse:mutation\" - how a request is named on the card. */\nexport function requestLabel(request: any): string {\n const who = request?.requester?.name || request?.requester?.email || ''\n return [who, request?.category && request?.privilege ? `${request.category}:${request.privilege}` : ''].filter(Boolean).join(' · ')\n}\n\n/** The pending requests the said text names - by the requester's name or e-mail, or the privilege. Several stay several. */\nexport function requestsNamed(items: any[], said: string): { id: string; label: string }[] {\n const text = (said || '').trim().toLowerCase()\n if (!text) return []\n return (items || [])\n .filter(item => item?.state === 'PENDING')\n .filter(item => {\n if (item.id === said.trim()) return true\n const names = [item.requester?.name, item.requester?.email, `${item.category}:${item.privilege}`, requestLabel(item)]\n return names.some(name => typeof name === 'string' && name.toLowerCase().includes(text))\n })\n .map(item => ({ id: item.id, label: requestLabel(item) }))\n}\n\n/** A role named exactly - a near name is not taken. */\nexport function rolesNamed(items: any[], said: string): { id: string; label: string }[] {\n const text = (said || '').trim().toLowerCase()\n return (items || []).filter(item => typeof item?.name === 'string' && item.name.toLowerCase() === text).map(item => ({ id: item.id, label: item.name }))\n}\n\nconst DECISION_FIELDS: EntryField[] = [\n {\n name: 'request',\n type: 'reference',\n required: true,\n labelKey: 'access-request.field.request',\n description: \"the pending access request, by the requester's name and the privilege asked for\",\n reference: {\n query: PENDING_REQUESTS,\n variables: () => ({ filters: [{ name: 'state', operator: 'eq', value: 'PENDING' }], pagination: { page: 1, limit: 500 } }),\n recordsOf: (data, said) => requestsNamed(data?.accessRequests?.items ?? [], said)\n }\n },\n {\n name: 'decision',\n type: 'enum',\n required: true,\n values: ['GRANT', 'DENY'],\n labelKey: 'access-request.field.decision',\n valueKeyPrefix: 'access-request.decision.',\n description: 'GRANT puts the requester into a role; DENY refuses with a reason'\n },\n {\n name: 'role',\n type: 'reference',\n labelKey: 'access-request.field.granted role',\n description:\n 'the role to put the requester into, by its exact name - only for GRANT. Prefer the role with the fewest privileges that still holds the one asked for; the review names a smaller one when there is.',\n reference: {\n query: ROLES_NAMED,\n variables: (said: string) => ({ name: said.trim() }),\n recordsOf: (data, said) => rolesNamed(data?.assignableRolesNamed ?? [], said)\n }\n },\n {\n name: 'reason',\n type: 'text',\n labelKey: 'access-request.field.decision reason',\n description: 'why - required for DENY. Never written by the model: ask the person.'\n }\n]\n\nconst idOf = (value: unknown) => (value && typeof value === 'object' && typeof (value as any).id === 'string' ? (value as any).id : undefined)\n\n/** A row as the decision door takes it. */\nexport function decisionOf(row: EntryRow) {\n const decision = row.values.decision === 'DENY' ? 'DENY' : 'GRANT'\n return {\n requestId: idOf(row.values.request),\n decision,\n roleId: decision === 'GRANT' ? idOf(row.values.role) : undefined,\n reason: typeof row.values.reason === 'string' ? row.values.reason : undefined\n }\n}\n\n/** The review's findings as the card's issues - the words are the screen's (`access-request.issue.<code>`). */\nexport function issuesOf(findings: { code: string; blocking: boolean; field?: string; params?: Record<string, unknown> }[]): EntryIssue[] {\n return findings.map(finding => ({\n ...(finding.field && finding.field !== 'decision' ? { field: finding.field } : {}),\n code: finding.code,\n messageKey: `access-request.issue.${finding.code}`,\n params: finding.params,\n blocking: finding.blocking\n }))\n}\n\nexport function accessDecisionEntryTarget(): EntryTargetDeclaration {\n return {\n key: AUTH_ACCESS_DECISION,\n labelKey: 'access-request.title.decision draft',\n description:\n 'one decision on a pending access request: which request, grant or deny, the role to grant with, and the reason to deny.',\n mutation: 'mutation ($decision: AccessDecisionInput!) { decideAccessRequest(decision: $decision) { id state } }',\n fields: DECISION_FIELDS,\n maxRows: 50,\n variables: row => ({ decision: decisionOf(row) }),\n appliedOf: (data, row) => ({ id: data?.decideAccessRequest?.id, label: (row.values.request as any)?.label }),\n\n check: async (rows, { state }) => {\n const { reviewDecision } = await import('../controllers/access-request.js')\n const out: EntryIssue[][] = []\n for (const row of rows) {\n const decision = decisionOf(row)\n out.push(decision.requestId ? issuesOf(await reviewDecision(decision as any, { state } as any)) : [])\n }\n return out\n }\n }\n}\n\nconst REQUEST_FIELDS: EntryField[] = [\n { name: 'category', type: 'string', required: true, labelKey: 'access-request.field.category', description: 'the category of the privilege, as the refusal named it' },\n { name: 'privilege', type: 'string', required: true, labelKey: 'access-request.field.privilege', description: 'the privilege, as the refusal named it - query, mutation, ...' },\n { name: 'reason', type: 'text', required: true, labelKey: 'access-request.field.reason', description: \"why the person needs it, in the person's words - ask them; never write it yourself\" }\n]\n\nexport function accessRequestEntryTarget(): EntryTargetDeclaration {\n return {\n key: AUTH_ACCESS_REQUEST,\n labelKey: 'access-request.title.request draft',\n description: 'one request for a privilege the person was refused for want of a role.',\n mutation: 'mutation ($request: NewAccessRequest!) { requestAccess(request: $request) { id count } }',\n fields: REQUEST_FIELDS,\n maxRows: 10,\n variables: row => ({ request: { category: row.values.category, privilege: row.values.privilege, reason: row.values.reason } }),\n appliedOf: data => ({ id: data?.requestAccess?.id }),\n\n /* What the door would refuse - asked before the card, so the person is not offered a request that cannot stand. */\n check: async (rows, { state }) => {\n const { judgePermission } = await import('../utils/check-permission.js')\n const out: EntryIssue[][] = []\n for (const row of rows) {\n const category = String(row.values.category ?? '')\n const privilege = String(row.values.privilege ?? '')\n if (!category || !privilege) {\n out.push([])\n continue\n }\n const judgement = await judgePermission({ category, privilege }, state?.user, state?.domain, state?.unsafeIP, state?.prohibitedPrivileges)\n out.push(\n judgement.permitted === true\n ? [{ code: 'already-holds', messageKey: 'access-request.issue.already-holds', blocking: true }]\n : judgement.because !== 'role'\n ? [{ code: 'not-by-role', messageKey: 'access-request.issue.not-by-role', params: { because: judgement.because }, blocking: true }]\n : []\n )\n }\n return out\n }\n }\n}\n\nexport function accessEntryTargets(): EntryTargetDeclaration[] {\n return [accessDecisionEntryTarget(), accessRequestEntryTarget()]\n}\n"]}
@@ -0,0 +1,82 @@
1
+ export type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'datetime' | 'boolean' | 'enum' | 'reference';
2
+ export interface EntryReference {
3
+ query: string;
4
+ variables(text: string): Record<string, unknown>;
5
+ recordsOf(data: any, text: string): {
6
+ id: string;
7
+ label?: string;
8
+ }[];
9
+ }
10
+ export interface EntryField {
11
+ name: string;
12
+ type: EntryFieldType;
13
+ required?: boolean;
14
+ values?: string[];
15
+ description?: string;
16
+ labelKey?: string;
17
+ total?: boolean;
18
+ valueKeyPrefix?: string;
19
+ reference?: EntryReference;
20
+ }
21
+ export interface EntryIssue {
22
+ field?: string;
23
+ code: string;
24
+ messageKey?: string;
25
+ params?: Record<string, unknown>;
26
+ blocking: boolean;
27
+ }
28
+ export interface EntryRow {
29
+ index: number;
30
+ values: Record<string, unknown>;
31
+ sources: unknown[];
32
+ issues: EntryIssue[];
33
+ }
34
+ export type EntryRecord = {
35
+ id: string;
36
+ } & Record<string, unknown>;
37
+ export interface EntryUpdate {
38
+ read: {
39
+ query: string;
40
+ variables(ids: string[]): Record<string, unknown>;
41
+ recordsOf(data: any, ids: string[]): EntryRecord[];
42
+ };
43
+ find?: {
44
+ query: string;
45
+ variables(criteria: Record<string, unknown>): Record<string, unknown>;
46
+ recordsOf(data: any, criteria: Record<string, unknown>): EntryRecord[];
47
+ criteria: EntryField[];
48
+ };
49
+ mutation: string;
50
+ variables(change: {
51
+ id: string;
52
+ record: EntryRecord;
53
+ values: Record<string, unknown>;
54
+ }): Record<string, unknown>;
55
+ labelOf?(record: EntryRecord): string;
56
+ fields?: string[];
57
+ /** Read-only checks of the changes, asked when the card is made and again when it is pressed - one issue list per row. */
58
+ check?(rows: {
59
+ id: string;
60
+ record?: EntryRecord;
61
+ values: Record<string, unknown>;
62
+ }[], context: {
63
+ state: any;
64
+ }): EntryIssue[][] | Promise<EntryIssue[][]>;
65
+ }
66
+ export interface EntryTargetDeclaration {
67
+ key: string;
68
+ labelKey: string;
69
+ description: string;
70
+ fields: EntryField[];
71
+ mutation: string;
72
+ variables(row: EntryRow): Record<string, unknown>;
73
+ appliedOf?(data: any, row: EntryRow): {
74
+ id?: string;
75
+ label?: string;
76
+ } | undefined;
77
+ maxRows?: number;
78
+ check?(rows: EntryRow[], context: {
79
+ state: any;
80
+ }): EntryIssue[][] | Promise<EntryIssue[][]>;
81
+ update?: EntryUpdate;
82
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=entry-target-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-target-shape.js","sourceRoot":"","sources":["../../server/ai-entry/entry-target-shape.ts"],"names":[],"mappings":"","sourcesContent":["/*\n * The shape of an AI 입력 (data entry) target - the same shape as `EntryTarget` in\n * @things-factory/ai-assistant (server/service/data-entry/entry-target.ts).\n *\n * Kept here as a structural copy (the same one organization keeps) so that this package declares its targets as plain data and does not\n * depend on the assistant at run time. An application that runs the assistant passes these declarations\n * to `registerEntryTarget`; the assistant validates them at boot.\n */\nexport type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'datetime' | 'boolean' | 'enum' | 'reference'\n\nexport interface EntryReference {\n query: string\n variables(text: string): Record<string, unknown>\n recordsOf(data: any, text: string): { id: string; label?: string }[]\n}\n\nexport interface EntryField {\n name: string\n type: EntryFieldType\n required?: boolean\n values?: string[]\n description?: string\n labelKey?: string\n total?: boolean\n valueKeyPrefix?: string\n reference?: EntryReference\n}\n\nexport interface EntryIssue {\n field?: string\n code: string\n messageKey?: string\n params?: Record<string, unknown>\n blocking: boolean\n}\n\nexport interface EntryRow {\n index: number\n values: Record<string, unknown>\n sources: unknown[]\n issues: EntryIssue[]\n}\n\nexport type EntryRecord = { id: string } & Record<string, unknown>\n\nexport interface EntryUpdate {\n read: { query: string; variables(ids: string[]): Record<string, unknown>; recordsOf(data: any, ids: string[]): EntryRecord[] }\n find?: {\n query: string\n variables(criteria: Record<string, unknown>): Record<string, unknown>\n recordsOf(data: any, criteria: Record<string, unknown>): EntryRecord[]\n criteria: EntryField[]\n }\n mutation: string\n variables(change: { id: string; record: EntryRecord; values: Record<string, unknown> }): Record<string, unknown>\n labelOf?(record: EntryRecord): string\n fields?: string[]\n /** Read-only checks of the changes, asked when the card is made and again when it is pressed - one issue list per row. */\n check?(rows: { id: string; record?: EntryRecord; values: Record<string, unknown> }[], context: { state: any }): EntryIssue[][] | Promise<EntryIssue[][]>\n}\n\nexport interface EntryTargetDeclaration {\n key: string\n labelKey: string\n description: string\n fields: EntryField[]\n mutation: string\n variables(row: EntryRow): Record<string, unknown>\n appliedOf?(data: any, row: EntryRow): { id?: string; label?: string } | undefined\n maxRows?: number\n check?(rows: EntryRow[], context: { state: any }): EntryIssue[][] | Promise<EntryIssue[][]>\n update?: EntryUpdate\n}\n"]}
@@ -27,6 +27,12 @@ export declare class AccessDecisionRefused extends Refusal {
27
27
  * taken on trust (ADR-0103 decision 2). Asking again while one is pending adds to that one.
28
28
  */
29
29
  export declare function requestAccess(input: NewAccessRequestInput, context: ResolverContext): Promise<AccessRequest>;
30
+ /** A request withdrawn within this many hours comes back as the same row, without telling anyone again. */
31
+ export declare const ACCESS_REQUEST_QUIET_HOURS = 24;
32
+ /** How long a denial is shown back to the person and holds against the same words (questions Q9 · 5). */
33
+ export declare const ACCESS_REQUEST_DENIAL_DAYS = 7;
34
+ /** The latest denial of this person and privilege within ACCESS_REQUEST_DENIAL_DAYS, if any. */
35
+ export declare function latestDenial(domain: Domain, requesterId: string, category: string, privilege: string, now: Date, tx?: EntityManager): Promise<AccessRequest>;
30
36
  /** The requester takes back their own pending request. */
31
37
  export declare function withdrawAccessRequest(id: string, context: ResolverContext): Promise<AccessRequest>;
32
38
  /**