@things-factory/auth-ui 10.1.106 → 10.1.107

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 (31) hide show
  1. package/dist-client/bootstrap.js +22 -1
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/access-decision-popup.d.ts +30 -0
  4. package/dist-client/components/access-decision-popup.js +228 -0
  5. package/dist-client/components/access-decision-popup.js.map +1 -0
  6. package/dist-client/components/access-request-popup.d.ts +32 -0
  7. package/dist-client/components/access-request-popup.js +211 -0
  8. package/dist-client/components/access-request-popup.js.map +1 -0
  9. package/dist-client/pages/access-request/access-requests.d.ts +51 -0
  10. package/dist-client/pages/access-request/access-requests.js +220 -0
  11. package/dist-client/pages/access-request/access-requests.js.map +1 -0
  12. package/dist-client/pages/access-request/my-access-requests.d.ts +26 -0
  13. package/dist-client/pages/access-request/my-access-requests.js +151 -0
  14. package/dist-client/pages/access-request/my-access-requests.js.map +1 -0
  15. package/dist-client/route.js +6 -0
  16. package/dist-client/route.js.map +1 -1
  17. package/dist-client/tsconfig.layout-check.tsbuildinfo +1 -0
  18. package/dist-client/tsconfig.tsbuildinfo +1 -1
  19. package/dist-client/utils/access-request.d.ts +49 -0
  20. package/dist-client/utils/access-request.js +67 -0
  21. package/dist-client/utils/access-request.js.map +1 -0
  22. package/dist-client/utils/access-request.test.d.ts +1 -0
  23. package/dist-client/utils/access-request.test.js +70 -0
  24. package/dist-client/utils/access-request.test.js.map +1 -0
  25. package/package.json +5 -5
  26. package/things-factory.config.js +3 -1
  27. package/translations/en.json +61 -1
  28. package/translations/ja.json +61 -1
  29. package/translations/ko.json +61 -1
  30. package/translations/ms.json +61 -1
  31. package/translations/zh.json +61 -1
@@ -0,0 +1,49 @@
1
+ export type PrivilegeRequestDetail = {
2
+ category: string;
3
+ privilege: string;
4
+ operationName?: string;
5
+ };
6
+ /**
7
+ * What a refusal offers to ask for - only a refusal a role would open. Address, license, ownership and
8
+ * unknown refusals offer nothing: giving a role would not open them.
9
+ */
10
+ export declare function requestableFrom(extensions: any, operationName?: string): PrivilegeRequestDetail | null;
11
+ type Translate = (key: string, options?: Record<string, unknown>) => string;
12
+ /**
13
+ * What a privilege lets one do, in the reader's words - the sentence the role screen shows
14
+ * (`privilege.allows.<category>.<privilege>`), or the name itself when none is written. Never made up.
15
+ */
16
+ export declare function privilegeWords(t: Translate, category: string, privilege: string): string;
17
+ export type ReviewIssue = {
18
+ code: string;
19
+ blocking: boolean;
20
+ field?: string;
21
+ params?: Record<string, unknown>;
22
+ };
23
+ /** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */
24
+ export declare function issueWords(t: Translate, issue: ReviewIssue): string;
25
+ export declare const blocks: (issues: ReviewIssue[]) => boolean;
26
+ /** Which buttons a decision popup offers, from the review the server gave. */
27
+ export declare function decisionButtons(grantIssues: ReviewIssue[] | null, denyIssues: ReviewIssue[] | null): {
28
+ grant: boolean;
29
+ deny: boolean;
30
+ };
31
+ /** The candidate a popup selects first - the smallest; ties by name, as the server sorts them. */
32
+ export declare function firstCandidate<T extends {
33
+ name: string;
34
+ privilegeCount: number;
35
+ }>(candidates: T[]): T | undefined;
36
+ /** How long a denial is shown back before asking again - the server holds the same words against it (auth-base ACCESS_REQUEST_DENIAL_DAYS). */
37
+ export declare const DENIAL_SHOWN_DAYS = 7;
38
+ /** The latest denial of this privilege among the person's own requests, within DENIAL_SHOWN_DAYS. */
39
+ export declare function recentDenial<T extends {
40
+ state: string;
41
+ category: string;
42
+ privilege: string;
43
+ decidedAt?: string | null;
44
+ }>(mine: T[], category: string, privilege: string, now?: number): T | undefined;
45
+ /** Send is offered only with a reason - and, after a denial, with words other than the denied ones. */
46
+ export declare function maySend(reason: string, denied?: {
47
+ reason?: string | null;
48
+ }): boolean;
49
+ export {};
@@ -0,0 +1,67 @@
1
+ /*
2
+ * The access request on the screen - pure, so the rules are tested without a browser (ADR-0103).
3
+ *
4
+ * A refusal's toast gets its button from this package through @operato/layout's registerNotifyAction
5
+ * (bootstrap.ts) - the toast and @operato/graphql do not know what a refusal means (ADR-0103 decision 5).
6
+ */
7
+ /**
8
+ * What a refusal offers to ask for - only a refusal a role would open. Address, license, ownership and
9
+ * unknown refusals offer nothing: giving a role would not open them.
10
+ */
11
+ export function requestableFrom(extensions, operationName) {
12
+ if (!extensions || extensions.code !== 'FORBIDDEN' || extensions.because !== 'role')
13
+ return null;
14
+ /*
15
+ * Only a change the person made (questions Q9 · 4). A page fires several reads as it opens; offering a request on
16
+ * each would put the button in front of someone who did not choose to do anything yet.
17
+ */
18
+ if (extensions.operation !== 'mutation')
19
+ return null;
20
+ const requires = extensions.requires;
21
+ if (!requires || typeof requires !== 'object' || !requires.category || !requires.privilege)
22
+ return null;
23
+ return { category: requires.category, privilege: requires.privilege, ...(operationName ? { operationName } : {}) };
24
+ }
25
+ /**
26
+ * What a privilege lets one do, in the reader's words - the sentence the role screen shows
27
+ * (`privilege.allows.<category>.<privilege>`), or the name itself when none is written. Never made up.
28
+ */
29
+ export function privilegeWords(t, category, privilege) {
30
+ return t(`privilege.allows.${category}.${privilege}`, { defaultValue: '' }) || `${category}:${privilege}`;
31
+ }
32
+ /** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */
33
+ export function issueWords(t, issue) {
34
+ const params = { ...(issue.params || {}) };
35
+ if (typeof params.state === 'string')
36
+ params.state = t(`access-request.state.${String(params.state).toLowerCase()}`, { defaultValue: String(params.state) });
37
+ return t(`access-request.issue.${issue.code}`, { ...params, defaultValue: issue.code });
38
+ }
39
+ export const blocks = (issues) => issues.some(issue => issue.blocking);
40
+ /** Which buttons a decision popup offers, from the review the server gave. */
41
+ export function decisionButtons(grantIssues, denyIssues) {
42
+ return {
43
+ grant: !!grantIssues && !blocks(grantIssues),
44
+ deny: !!denyIssues && !blocks(denyIssues)
45
+ };
46
+ }
47
+ /** The candidate a popup selects first - the smallest; ties by name, as the server sorts them. */
48
+ export function firstCandidate(candidates) {
49
+ return [...(candidates || [])].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
50
+ }
51
+ /** How long a denial is shown back before asking again - the server holds the same words against it (auth-base ACCESS_REQUEST_DENIAL_DAYS). */
52
+ export const DENIAL_SHOWN_DAYS = 7;
53
+ /** The latest denial of this privilege among the person's own requests, within DENIAL_SHOWN_DAYS. */
54
+ export function recentDenial(mine, category, privilege, now = Date.now()) {
55
+ return (mine || [])
56
+ .filter(r => r.state === 'DENIED' && r.category === category && r.privilege === privilege && r.decidedAt)
57
+ .filter(r => now - new Date(r.decidedAt).getTime() <= DENIAL_SHOWN_DAYS * 24 * 60 * 60 * 1000)
58
+ .sort((a, b) => new Date(b.decidedAt).getTime() - new Date(a.decidedAt).getTime())[0];
59
+ }
60
+ /** Send is offered only with a reason - and, after a denial, with words other than the denied ones. */
61
+ export function maySend(reason, denied) {
62
+ const said = (reason || '').trim();
63
+ if (!said)
64
+ return false;
65
+ return !(denied && (denied.reason || '').trim() === said);
66
+ }
67
+ //# sourceMappingURL=access-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-request.js","sourceRoot":"","sources":["../../client/utils/access-request.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,UAAe,EAAE,aAAsB;IACrE,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,IAAI,UAAU,CAAC,OAAO,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAChG;;;OAGG;IACH,IAAI,UAAU,CAAC,SAAS,KAAK,UAAU;QAAE,OAAO,IAAI,CAAA;IACpD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAA;IACpC,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,SAAS;QAAE,OAAO,IAAI,CAAA;IACvG,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AACpH,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,CAAY,EAAE,QAAgB,EAAE,SAAiB;IAC9E,OAAO,CAAC,CAAC,oBAAoB,QAAQ,IAAI,SAAS,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;AAC3G,CAAC;AAID,iHAAiH;AACjH,MAAM,UAAU,UAAU,CAAC,CAAY,EAAE,KAAkB;IACzD,MAAM,MAAM,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAA;IAC1C,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;QAAE,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,wBAAwB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAC5J,OAAO,CAAC,CAAC,wBAAwB,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;AACzF,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAqB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;AAErF,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAAC,WAAiC,EAAE,UAAgC;IACjG,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,WAAW,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QAC5C,IAAI,EAAE,CAAC,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;KAC1C,CAAA;AACH,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,cAAc,CAAqD,UAAe;IAChG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACvH,CAAC;AAED,+IAA+I;AAC/I,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAA;AAElC,qGAAqG;AACrG,MAAM,UAAU,YAAY,CAC1B,IAAS,EACT,QAAgB,EAChB,SAAiB,EACjB,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IAEhB,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC;SACxG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAmB,CAAC,CAAC,OAAO,EAAE,IAAI,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;SACvG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,SAAmB,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAmB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7G,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,MAAmC;IACzE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAClC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IACvB,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAA;AAC3D,CAAC","sourcesContent":["/*\n * The access request on the screen - pure, so the rules are tested without a browser (ADR-0103).\n *\n * A refusal's toast gets its button from this package through @operato/layout's registerNotifyAction\n * (bootstrap.ts) - the toast and @operato/graphql do not know what a refusal means (ADR-0103 decision 5).\n */\n\nexport type PrivilegeRequestDetail = { category: string; privilege: string; operationName?: string }\n\n/**\n * What a refusal offers to ask for - only a refusal a role would open. Address, license, ownership and\n * unknown refusals offer nothing: giving a role would not open them.\n */\nexport function requestableFrom(extensions: any, operationName?: string): PrivilegeRequestDetail | null {\n if (!extensions || extensions.code !== 'FORBIDDEN' || extensions.because !== 'role') return null\n /*\n * Only a change the person made (questions Q9 · 4). A page fires several reads as it opens; offering a request on\n * each would put the button in front of someone who did not choose to do anything yet.\n */\n if (extensions.operation !== 'mutation') return null\n const requires = extensions.requires\n if (!requires || typeof requires !== 'object' || !requires.category || !requires.privilege) return null\n return { category: requires.category, privilege: requires.privilege, ...(operationName ? { operationName } : {}) }\n}\n\ntype Translate = (key: string, options?: Record<string, unknown>) => string\n\n/**\n * What a privilege lets one do, in the reader's words - the sentence the role screen shows\n * (`privilege.allows.<category>.<privilege>`), or the name itself when none is written. Never made up.\n */\nexport function privilegeWords(t: Translate, category: string, privilege: string): string {\n return t(`privilege.allows.${category}.${privilege}`, { defaultValue: '' }) || `${category}:${privilege}`\n}\n\nexport type ReviewIssue = { code: string; blocking: boolean; field?: string; params?: Record<string, unknown> }\n\n/** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */\nexport function issueWords(t: Translate, issue: ReviewIssue): string {\n const params = { ...(issue.params || {}) }\n if (typeof params.state === 'string') params.state = t(`access-request.state.${String(params.state).toLowerCase()}`, { defaultValue: String(params.state) })\n return t(`access-request.issue.${issue.code}`, { ...params, defaultValue: issue.code })\n}\n\nexport const blocks = (issues: ReviewIssue[]) => issues.some(issue => issue.blocking)\n\n/** Which buttons a decision popup offers, from the review the server gave. */\nexport function decisionButtons(grantIssues: ReviewIssue[] | null, denyIssues: ReviewIssue[] | null) {\n return {\n grant: !!grantIssues && !blocks(grantIssues),\n deny: !!denyIssues && !blocks(denyIssues)\n }\n}\n\n/** The candidate a popup selects first - the smallest; ties by name, as the server sorts them. */\nexport function firstCandidate<T extends { name: string; privilegeCount: number }>(candidates: T[]): T | undefined {\n return [...(candidates || [])].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n}\n\n/** How long a denial is shown back before asking again - the server holds the same words against it (auth-base ACCESS_REQUEST_DENIAL_DAYS). */\nexport const DENIAL_SHOWN_DAYS = 7\n\n/** The latest denial of this privilege among the person's own requests, within DENIAL_SHOWN_DAYS. */\nexport function recentDenial<T extends { state: string; category: string; privilege: string; decidedAt?: string | null }>(\n mine: T[],\n category: string,\n privilege: string,\n now = Date.now()\n): T | undefined {\n return (mine || [])\n .filter(r => r.state === 'DENIED' && r.category === category && r.privilege === privilege && r.decidedAt)\n .filter(r => now - new Date(r.decidedAt as string).getTime() <= DENIAL_SHOWN_DAYS * 24 * 60 * 60 * 1000)\n .sort((a, b) => new Date(b.decidedAt as string).getTime() - new Date(a.decidedAt as string).getTime())[0]\n}\n\n/** Send is offered only with a reason - and, after a denial, with words other than the denied ones. */\nexport function maySend(reason: string, denied?: { reason?: string | null }): boolean {\n const said = (reason || '').trim()\n if (!said) return false\n return !(denied && (denied.reason || '').trim() === said)\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { blocks, decisionButtons, firstCandidate, issueWords, maySend, privilegeWords, recentDenial, requestableFrom } from './access-request';
2
+ /**
3
+ * The access request on the screen (ADR-0103). The refusal says why; only a missing role offers a request.
4
+ */
5
+ const ROLE = { code: 'FORBIDDEN', requires: { category: 'warehouse', privilege: 'mutation' }, because: 'role', operation: 'mutation' };
6
+ describe('what a refusal offers to ask for', () => {
7
+ it('★ a refusal a role would open - the privilege it named', () => {
8
+ expect(requestableFrom(ROLE, 'updateInboundPlan')).toEqual({ category: 'warehouse', privilege: 'mutation', operationName: 'updateInboundPlan' });
9
+ });
10
+ it('★ address, license, ownership and unknown offer nothing - a role would not open them', () => {
11
+ for (const because of ['address', 'license', 'ownership', 'unknown']) {
12
+ expect(requestableFrom({ ...ROLE, because })).toBeNull();
13
+ }
14
+ });
15
+ it('★ a read the page fired offers nothing - only a change the person made (Q9)', () => {
16
+ expect(requestableFrom({ ...ROLE, operation: 'query' })).toBeNull();
17
+ expect(requestableFrom({ ...ROLE, operation: 'subscription' })).toBeNull();
18
+ expect(requestableFrom({ ...ROLE, operation: undefined })).toBeNull();
19
+ });
20
+ it('a refusal from before the cause was carried offers nothing - it is not guessed', () => {
21
+ expect(requestableFrom({ code: 'FORBIDDEN', requires: ROLE.requires })).toBeNull();
22
+ });
23
+ it('ownership-only and other errors offer nothing', () => {
24
+ expect(requestableFrom({ ...ROLE, requires: 'ownership' })).toBeNull();
25
+ expect(requestableFrom({ code: 'BAD_USER_INPUT' })).toBeNull();
26
+ expect(requestableFrom(undefined)).toBeNull();
27
+ });
28
+ });
29
+ describe('words', () => {
30
+ const t = (key, options = {}) => (key === 'privilege.allows.warehouse.mutation' ? '창고 기록을 바꿀 수 있습니다.' : options.defaultValue);
31
+ it('★ the sentence the role screen writes, or the name - never a key', () => {
32
+ expect(privilegeWords(t, 'warehouse', 'mutation')).toBe('창고 기록을 바꿀 수 있습니다.');
33
+ expect(privilegeWords(t, 'board', 'query')).toBe('board:query');
34
+ });
35
+ it('an issue without a sentence shows its code, not the key', () => {
36
+ expect(issueWords(t, { code: 'own-request', blocking: true })).toBe('own-request');
37
+ });
38
+ });
39
+ describe('the decision buttons', () => {
40
+ it('★ a blocking finding takes the button away; a warning does not', () => {
41
+ expect(decisionButtons([{ code: 'role-grants-more', blocking: false }], [{ code: 'deny-needs-reason', blocking: true }])).toEqual({ grant: true, deny: false });
42
+ expect(decisionButtons([{ code: 'role-does-not-grant', blocking: true }], [])).toEqual({ grant: false, deny: true });
43
+ expect(decisionButtons(null, [])).toEqual({ grant: false, deny: true });
44
+ expect(blocks([])).toBe(false);
45
+ });
46
+ it('the smallest role first, ties by name', () => {
47
+ expect(firstCandidate([{ name: 'b', privilegeCount: 1 }, { name: 'a', privilegeCount: 1 }, { name: 'c', privilegeCount: 0 }])?.name).toBe('c');
48
+ });
49
+ });
50
+ describe('asking is not a reflex (Q9)', () => {
51
+ const NOW = new Date('2026-10-02T09:00:00Z').getTime();
52
+ const mine = [
53
+ { state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-30T09:00:00Z', reason: 'inbound', decisionReason: 'IT only' },
54
+ { state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-01T09:00:00Z', reason: 'old', decisionReason: 'old' },
55
+ { state: 'DENIED', category: 'board', privilege: 'mutation', decidedAt: '2026-10-01T09:00:00Z', reason: 'x', decisionReason: 'y' }
56
+ ];
57
+ it('★ the latest denial of the same privilege within seven days is shown back', () => {
58
+ expect(recentDenial(mine, 'warehouse', 'mutation', NOW)?.decisionReason).toBe('IT only');
59
+ expect(recentDenial(mine, 'warehouse', 'query', NOW)).toBeUndefined();
60
+ expect(recentDenial(mine.slice(1, 2), 'warehouse', 'mutation', NOW)).toBeUndefined();
61
+ });
62
+ it('★ send needs a reason, and after a denial other words than the denied ones', () => {
63
+ expect(maySend('')).toBe(false);
64
+ expect(maySend(' ')).toBe(false);
65
+ expect(maySend('inbound')).toBe(true);
66
+ expect(maySend('inbound', { reason: 'inbound' })).toBe(false);
67
+ expect(maySend('moved to inbound', { reason: 'inbound' })).toBe(true);
68
+ });
69
+ });
70
+ //# sourceMappingURL=access-request.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-request.test.js","sourceRoot":"","sources":["../../client/utils/access-request.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAE9I;;GAEG;AAEH,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;AAEtI,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAChD,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC,CAAA;IAClJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;QAC9F,KAAK,MAAM,OAAO,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC;YACrE,MAAM,CAAC,eAAe,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QAC1D,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACrF,MAAM,CAAC,eAAe,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QACnE,MAAM,CAAC,eAAe,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QAC1E,MAAM,CAAC,eAAe,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QACxF,MAAM,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACpF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,eAAe,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QACtE,MAAM,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QAC9D,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACrB,MAAM,CAAC,GAAG,CAAC,GAAW,EAAE,UAAe,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,qCAAqC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IAE1I,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QAC5E,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IACpF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QAC/J,MAAM,CAAC,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACpH,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACvE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAChJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,OAAO,EAAE,CAAA;IACtD,MAAM,IAAI,GAAG;QACX,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,EAAE;QAClJ,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;QAC1I,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE;KACnI,CAAA;IAED,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACnF,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,cAAc,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACxF,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;QACrE,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/B,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7D,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { blocks, decisionButtons, firstCandidate, issueWords, maySend, privilegeWords, recentDenial, requestableFrom } from './access-request'\n\n/**\n * The access request on the screen (ADR-0103). The refusal says why; only a missing role offers a request.\n */\n\nconst ROLE = { code: 'FORBIDDEN', requires: { category: 'warehouse', privilege: 'mutation' }, because: 'role', operation: 'mutation' }\n\ndescribe('what a refusal offers to ask for', () => {\n it('★ a refusal a role would open - the privilege it named', () => {\n expect(requestableFrom(ROLE, 'updateInboundPlan')).toEqual({ category: 'warehouse', privilege: 'mutation', operationName: 'updateInboundPlan' })\n })\n\n it('★ address, license, ownership and unknown offer nothing - a role would not open them', () => {\n for (const because of ['address', 'license', 'ownership', 'unknown']) {\n expect(requestableFrom({ ...ROLE, because })).toBeNull()\n }\n })\n\n it('★ a read the page fired offers nothing - only a change the person made (Q9)', () => {\n expect(requestableFrom({ ...ROLE, operation: 'query' })).toBeNull()\n expect(requestableFrom({ ...ROLE, operation: 'subscription' })).toBeNull()\n expect(requestableFrom({ ...ROLE, operation: undefined })).toBeNull()\n })\n\n it('a refusal from before the cause was carried offers nothing - it is not guessed', () => {\n expect(requestableFrom({ code: 'FORBIDDEN', requires: ROLE.requires })).toBeNull()\n })\n\n it('ownership-only and other errors offer nothing', () => {\n expect(requestableFrom({ ...ROLE, requires: 'ownership' })).toBeNull()\n expect(requestableFrom({ code: 'BAD_USER_INPUT' })).toBeNull()\n expect(requestableFrom(undefined)).toBeNull()\n })\n})\n\ndescribe('words', () => {\n const t = (key: string, options: any = {}) => (key === 'privilege.allows.warehouse.mutation' ? '창고 기록을 바꿀 수 있습니다.' : options.defaultValue)\n\n it('★ the sentence the role screen writes, or the name - never a key', () => {\n expect(privilegeWords(t, 'warehouse', 'mutation')).toBe('창고 기록을 바꿀 수 있습니다.')\n expect(privilegeWords(t, 'board', 'query')).toBe('board:query')\n })\n\n it('an issue without a sentence shows its code, not the key', () => {\n expect(issueWords(t, { code: 'own-request', blocking: true })).toBe('own-request')\n })\n})\n\ndescribe('the decision buttons', () => {\n it('★ a blocking finding takes the button away; a warning does not', () => {\n expect(decisionButtons([{ code: 'role-grants-more', blocking: false }], [{ code: 'deny-needs-reason', blocking: true }])).toEqual({ grant: true, deny: false })\n expect(decisionButtons([{ code: 'role-does-not-grant', blocking: true }], [])).toEqual({ grant: false, deny: true })\n expect(decisionButtons(null, [])).toEqual({ grant: false, deny: true })\n expect(blocks([])).toBe(false)\n })\n\n it('the smallest role first, ties by name', () => {\n expect(firstCandidate([{ name: 'b', privilegeCount: 1 }, { name: 'a', privilegeCount: 1 }, { name: 'c', privilegeCount: 0 }])?.name).toBe('c')\n })\n})\n\ndescribe('asking is not a reflex (Q9)', () => {\n const NOW = new Date('2026-10-02T09:00:00Z').getTime()\n const mine = [\n { state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-30T09:00:00Z', reason: 'inbound', decisionReason: 'IT only' },\n { state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-01T09:00:00Z', reason: 'old', decisionReason: 'old' },\n { state: 'DENIED', category: 'board', privilege: 'mutation', decidedAt: '2026-10-01T09:00:00Z', reason: 'x', decisionReason: 'y' }\n ]\n\n it('★ the latest denial of the same privilege within seven days is shown back', () => {\n expect(recentDenial(mine, 'warehouse', 'mutation', NOW)?.decisionReason).toBe('IT only')\n expect(recentDenial(mine, 'warehouse', 'query', NOW)).toBeUndefined()\n expect(recentDenial(mine.slice(1, 2), 'warehouse', 'mutation', NOW)).toBeUndefined()\n })\n\n it('★ send needs a reason, and after a denial other words than the denied ones', () => {\n expect(maySend('')).toBe(false)\n expect(maySend(' ')).toBe(false)\n expect(maySend('inbound')).toBe(true)\n expect(maySend('inbound', { reason: 'inbound' })).toBe(false)\n expect(maySend('moved to inbound', { reason: 'inbound' })).toBe(true)\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-ui",
3
- "version": "10.1.106",
3
+ "version": "10.1.107",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -31,7 +31,7 @@
31
31
  "@operato/attribute": "^10.0.0",
32
32
  "@operato/data-grist": "^10.19.0",
33
33
  "@operato/i18n": "^10.0.0",
34
- "@operato/layout": "^10.26.0",
34
+ "@operato/layout": "^10.29.5",
35
35
  "@operato/list-state": "^10.15.0",
36
36
  "@operato/lottie-player": "^10.0.0",
37
37
  "@operato/moment-timezone-es": "^10.0.0",
@@ -39,11 +39,11 @@
39
39
  "@operato/popup": "^10.15.0",
40
40
  "@operato/shell": "^10.29.2",
41
41
  "@operato/styles": "^10.0.0",
42
- "@things-factory/auth-base": "^10.1.106",
43
- "@things-factory/component-ui": "^10.1.106",
42
+ "@things-factory/auth-base": "^10.1.107",
43
+ "@things-factory/component-ui": "^10.1.107",
44
44
  "@things-factory/more-base": "^10.1.106",
45
45
  "base64url": "^3.0.1",
46
46
  "clipboard": "^2.0.6"
47
47
  },
48
- "gitHead": "8d78c14708420773ec32d18b8dd44c300771c44d"
48
+ "gitHead": "65a3d5889e687d8122a65f029b926b1a4769d1c0"
49
49
  }
@@ -18,7 +18,9 @@ export default {
18
18
  { tagname: 'env-var-list-page', page: 'env-vars' },
19
19
  { tagname: 'attribute-set-management', page: 'attributes' },
20
20
  { tagname: 'auth-provider-management', page: 'auth-providers' },
21
- { tagname: 'domain-link-management', page: 'domain-links' }
21
+ { tagname: 'domain-link-management', page: 'domain-links' },
22
+ { tagname: 'access-requests-page', page: 'access-requests' },
23
+ { tagname: 'my-access-requests-page', page: 'my-access-requests' }
22
24
  ],
23
25
  bootstrap
24
26
  }
@@ -349,5 +349,65 @@
349
349
  "title.secure-ip-list": "secured IP list",
350
350
  "title.sign in": "sign in",
351
351
  "title.sign up": "sign up",
352
- "title.unlock account": "unlock account"
352
+ "title.unlock account": "unlock account",
353
+ "access-request.title.access requests": "access requests",
354
+ "access-request.title.my access requests": "my access requests",
355
+ "access-request.title.decide": "decide the access request",
356
+ "access-request.title.request access": "request access",
357
+ "access-request.text.refused at": "refused at: {operation}",
358
+ "access-request.text.already waiting": "a request is already waiting (last asked {at}). sending adds to it.",
359
+ "access-request.text.sent": "sent to the people who decide access.",
360
+ "access-request.text.added to waiting": "added to the request already waiting.",
361
+ "access-request.text.who asks": "{name} asks for",
362
+ "access-request.text.grant with role": "grant by putting them into a role",
363
+ "access-request.text.no role grants it": "no role in this domain grants it. add it to a role on the role screen first.",
364
+ "access-request.text.privilege count": "{count} privileges",
365
+ "access-request.field.reason": "why it is needed",
366
+ "access-request.field.deny reason": "reason to deny",
367
+ "access-request.field.requester": "requester",
368
+ "access-request.field.needed": "needed",
369
+ "access-request.field.count": "times",
370
+ "access-request.field.decided by": "decided by",
371
+ "access-request.field.granted role": "role given",
372
+ "access-request.field.decision reason": "reason",
373
+ "access-request.field.decided at": "decided at",
374
+ "access-request.button.send": "send",
375
+ "access-request.button.grant": "grant",
376
+ "access-request.button.deny": "deny",
377
+ "access-request.button.decide": "decide",
378
+ "access-request.button.withdraw": "withdraw the request",
379
+ "access-request.state.pending": "waiting",
380
+ "access-request.state.granted": "granted",
381
+ "access-request.state.denied": "denied",
382
+ "access-request.state.withdrawn": "withdrawn",
383
+ "access-request.state.expired": "expired",
384
+ "access-request.issue.access-request-not-found": "the request is not found in this domain.",
385
+ "access-request.issue.request-not-pending": "this request is already {state}.",
386
+ "access-request.issue.own-request": "you cannot decide your own request.",
387
+ "access-request.issue.requester-unavailable": "the requester is locked, inactive or no longer in this domain.",
388
+ "access-request.issue.deny-needs-reason": "write why it is denied.",
389
+ "access-request.issue.already-holds": "the requester already has it - through another role.",
390
+ "access-request.issue.role-required": "choose a role.",
391
+ "access-request.issue.role-not-assignable": "this domain cannot assign that role.",
392
+ "access-request.issue.role-does-not-grant": "{role} does not hold {privilege}.",
393
+ "access-request.issue.role-grants-more": "{role} also gives {count} more: {privileges}",
394
+ "access-request.issue.role-grants-admin": "{role} lets them give roles to others.",
395
+ "access-request.issue.smaller-role-exists": "a smaller role grants it too: {role} ({count} privileges)",
396
+ "access-request.issue.decider-lacks-it": "you are giving what you do not have yourself: {privileges}",
397
+ "access-request.issue.denied-before": "denied before by {by} ({at}): {reason}",
398
+ "access-request.title.decision draft": "access decision draft",
399
+ "access-request.title.request draft": "access request draft",
400
+ "access-request.field.request": "request",
401
+ "access-request.field.decision": "decision",
402
+ "access-request.field.category": "category",
403
+ "access-request.field.privilege": "privilege",
404
+ "access-request.decision.grant": "grant",
405
+ "access-request.decision.deny": "deny",
406
+ "access-request.issue.not-by-role": "a role would not open this here ({because}).",
407
+ "access-request.dock.mode": "access requests",
408
+ "access-request.dock.placeholder": "e.g. sort out today's access requests",
409
+ "access-request.text.recorded and told": "the request is recorded and {count} people who decide access are told.",
410
+ "access-request.text.nobody to tell": "nobody in this domain decides access requests, so it cannot be sent. ask your administrator directly.",
411
+ "access-request.text.denied lately": "denied on {at}: {reason}. to ask again, write what has changed.",
412
+ "access-request.button.request": "request access…"
353
413
  }
@@ -349,5 +349,65 @@
349
349
  "text.x on": "{x}件オン",
350
350
  "text.x people hold this role": "このロールを持つ人 {x}名",
351
351
  "title.add domain owner": "$t(term.domain)のオーナーを追加",
352
- "title.role privileges": "ロールの権限"
352
+ "title.role privileges": "ロールの権限",
353
+ "access-request.title.access requests": "権限リクエスト",
354
+ "access-request.title.my access requests": "自分の権限リクエスト",
355
+ "access-request.title.decide": "権限リクエストの決定",
356
+ "access-request.title.request access": "権限をリクエスト",
357
+ "access-request.text.refused at": "止まった操作: {operation}",
358
+ "access-request.text.already waiting": "すでに送ったリクエストがあります(最終 {at})。送るとそのリクエストに追加されます。",
359
+ "access-request.text.sent": "権限を決める人に送りました。",
360
+ "access-request.text.added to waiting": "送信済みのリクエストに追加しました。",
361
+ "access-request.text.who asks": "{name} さんのリクエスト",
362
+ "access-request.text.grant with role": "ロールに追加して付与",
363
+ "access-request.text.no role grants it": "この権限を与えるロールがありません。先にロール画面でロールに追加してください。",
364
+ "access-request.text.privilege count": "権限 {count} 件",
365
+ "access-request.field.reason": "必要な理由",
366
+ "access-request.field.deny reason": "拒否の理由",
367
+ "access-request.field.requester": "リクエスト者",
368
+ "access-request.field.needed": "必要なもの",
369
+ "access-request.field.count": "回数",
370
+ "access-request.field.decided by": "決定者",
371
+ "access-request.field.granted role": "付与したロール",
372
+ "access-request.field.decision reason": "理由",
373
+ "access-request.field.decided at": "決定日時",
374
+ "access-request.button.send": "送信",
375
+ "access-request.button.grant": "付与",
376
+ "access-request.button.deny": "拒否",
377
+ "access-request.button.decide": "決定",
378
+ "access-request.button.withdraw": "リクエストを取り下げる",
379
+ "access-request.state.pending": "保留中",
380
+ "access-request.state.granted": "付与済み",
381
+ "access-request.state.denied": "拒否",
382
+ "access-request.state.withdrawn": "取り下げ",
383
+ "access-request.state.expired": "期限切れ",
384
+ "access-request.issue.access-request-not-found": "この$t(term.domain)にリクエストがありません。",
385
+ "access-request.issue.request-not-pending": "このリクエストはすでに{state}です。",
386
+ "access-request.issue.own-request": "自分のリクエストは決定できません。",
387
+ "access-request.issue.requester-unavailable": "リクエスト者がロック中、無効、またはこの$t(term.domain)にいません。",
388
+ "access-request.issue.deny-needs-reason": "拒否の理由を書いてください。",
389
+ "access-request.issue.already-holds": "リクエスト者はすでに別のロールでその権限を持っています。",
390
+ "access-request.issue.role-required": "ロールを選んでください。",
391
+ "access-request.issue.role-not-assignable": "この$t(term.domain)では付与できないロールです。",
392
+ "access-request.issue.role-does-not-grant": "{role} には {privilege} 権限がありません。",
393
+ "access-request.issue.role-grants-more": "{role} は要求以外に {count} 件を与えます: {privileges}",
394
+ "access-request.issue.role-grants-admin": "{role} を持つと他の人にロールを与えられるようになります。",
395
+ "access-request.issue.smaller-role-exists": "より小さいロールもこの権限を与えます: {role}(権限 {count} 件)",
396
+ "access-request.issue.decider-lacks-it": "自分にない権限を与えることになります: {privileges}",
397
+ "access-request.issue.denied-before": "以前に拒否されています — {by}、{at}: {reason}",
398
+ "access-request.title.decision draft": "権限リクエストの決定案",
399
+ "access-request.title.request draft": "権限リクエスト案",
400
+ "access-request.field.request": "リクエスト",
401
+ "access-request.field.decision": "決定",
402
+ "access-request.field.category": "カテゴリー",
403
+ "access-request.field.privilege": "権限",
404
+ "access-request.decision.grant": "付与",
405
+ "access-request.decision.deny": "拒否",
406
+ "access-request.issue.not-by-role": "ここではロールで開けない権限です({because})。",
407
+ "access-request.dock.mode": "権限リクエスト",
408
+ "access-request.dock.placeholder": "例: 今日届いた権限リクエストを整理して",
409
+ "access-request.text.recorded and told": "このリクエストは記録され、権限を決める管理者 {count} 名に通知されます。",
410
+ "access-request.text.nobody to tell": "この$t(term.domain)には権限を決める管理者がいないため送信できません。管理者に直接問い合わせてください。",
411
+ "access-request.text.denied lately": "{at} に拒否されました: {reason}。再度リクエストするには、何が変わったかを書いてください。",
412
+ "access-request.button.request": "権限をリクエスト…"
353
413
  }
@@ -349,5 +349,65 @@
349
349
  "title.secure-ip-list": "보안 IP 리스트",
350
350
  "title.sign in": "로그인",
351
351
  "title.sign up": "사용자 등록",
352
- "title.unlock account": "계정 잠금 해제"
352
+ "title.unlock account": "계정 잠금 해제",
353
+ "access-request.title.access requests": "권한 요청",
354
+ "access-request.title.my access requests": "내 권한 요청",
355
+ "access-request.title.decide": "권한 요청 결정",
356
+ "access-request.title.request access": "권한 요청",
357
+ "access-request.text.refused at": "막힌 일: {operation}",
358
+ "access-request.text.already waiting": "이미 보낸 요청이 있습니다(마지막 {at}). 보내면 그 요청에 더해집니다.",
359
+ "access-request.text.sent": "권한을 정하는 사람에게 보냈습니다.",
360
+ "access-request.text.added to waiting": "이미 보낸 요청에 더했습니다.",
361
+ "access-request.text.who asks": "{name} 님의 요청",
362
+ "access-request.text.grant with role": "역할에 더해서 부여",
363
+ "access-request.text.no role grants it": "이 권한을 주는 역할이 없습니다. 역할 화면에서 먼저 역할에 더하세요.",
364
+ "access-request.text.privilege count": "권한 {count}개",
365
+ "access-request.field.reason": "필요한 까닭",
366
+ "access-request.field.deny reason": "거부 까닭",
367
+ "access-request.field.requester": "요청자",
368
+ "access-request.field.needed": "필요한 것",
369
+ "access-request.field.count": "횟수",
370
+ "access-request.field.decided by": "정한 사람",
371
+ "access-request.field.granted role": "준 역할",
372
+ "access-request.field.decision reason": "까닭",
373
+ "access-request.field.decided at": "정한 시각",
374
+ "access-request.button.send": "보내기",
375
+ "access-request.button.grant": "부여",
376
+ "access-request.button.deny": "거부",
377
+ "access-request.button.decide": "결정",
378
+ "access-request.button.withdraw": "요청 거두기",
379
+ "access-request.state.pending": "기다림",
380
+ "access-request.state.granted": "부여됨",
381
+ "access-request.state.denied": "거부됨",
382
+ "access-request.state.withdrawn": "거둠",
383
+ "access-request.state.expired": "만료됨",
384
+ "access-request.issue.access-request-not-found": "이 $t(term.domain)에서 요청을 찾을 수 없습니다.",
385
+ "access-request.issue.request-not-pending": "이 요청은 이미 {state} 상태입니다.",
386
+ "access-request.issue.own-request": "자기 요청은 정할 수 없습니다.",
387
+ "access-request.issue.requester-unavailable": "요청자가 잠겼거나, 비활성이거나, 이 $t(term.domain)에서 빠졌습니다.",
388
+ "access-request.issue.deny-needs-reason": "거부 까닭을 적으세요.",
389
+ "access-request.issue.already-holds": "요청자가 이미 그 권한을 가지고 있습니다(다른 역할로).",
390
+ "access-request.issue.role-required": "역할을 고르세요.",
391
+ "access-request.issue.role-not-assignable": "이 $t(term.domain)에서 줄 수 없는 역할입니다.",
392
+ "access-request.issue.role-does-not-grant": "{role} 역할에는 {privilege} 권한이 없습니다.",
393
+ "access-request.issue.role-grants-more": "{role} 역할은 요청한 것 말고도 {count}개를 더 줍니다: {privileges}",
394
+ "access-request.issue.role-grants-admin": "{role} 역할을 받으면 남에게 권한을 줄 수 있게 됩니다.",
395
+ "access-request.issue.smaller-role-exists": "더 작은 역할도 이 권한을 줍니다: {role}(권한 {count}개)",
396
+ "access-request.issue.decider-lacks-it": "내게 없는 권한을 주게 됩니다: {privileges}",
397
+ "access-request.issue.denied-before": "전에 거부된 요청입니다 — {by}, {at}: {reason}",
398
+ "access-request.title.decision draft": "권한 요청 결정안",
399
+ "access-request.title.request draft": "권한 요청안",
400
+ "access-request.field.request": "요청",
401
+ "access-request.field.decision": "결정",
402
+ "access-request.field.category": "범주",
403
+ "access-request.field.privilege": "권한",
404
+ "access-request.decision.grant": "부여",
405
+ "access-request.decision.deny": "거부",
406
+ "access-request.issue.not-by-role": "여기서는 역할로 열리지 않는 권한입니다({because}).",
407
+ "access-request.dock.mode": "권한 요청",
408
+ "access-request.dock.placeholder": "예: 오늘 들어온 권한 요청 정리해 줘",
409
+ "access-request.text.recorded and told": "이 요청은 기록되고, 권한을 정하는 관리자 {count}명에게 알림이 갑니다.",
410
+ "access-request.text.nobody to tell": "이 $t(term.domain)에는 권한을 정할 관리자가 없어 보낼 수 없습니다. 관리자에게 직접 문의하세요.",
411
+ "access-request.text.denied lately": "{at}에 거부되었습니다: {reason}. 다시 요청하려면 무엇이 달라졌는지 적어 주세요.",
412
+ "access-request.button.request": "권한 요청…"
353
413
  }
@@ -349,5 +349,65 @@
349
349
  "text.x on": "{x} dihidupkan",
350
350
  "text.x people hold this role": "dipegang oleh {x}",
351
351
  "title.add domain owner": "tambah pemilik $t(term.domain)",
352
- "title.role privileges": "keistimewaan peranan"
352
+ "title.role privileges": "keistimewaan peranan",
353
+ "access-request.title.access requests": "permintaan akses",
354
+ "access-request.title.my access requests": "permintaan akses saya",
355
+ "access-request.title.decide": "putuskan permintaan akses",
356
+ "access-request.title.request access": "mohon akses",
357
+ "access-request.text.refused at": "ditolak pada: {operation}",
358
+ "access-request.text.already waiting": "permintaan sudah menunggu (terakhir {at}). menghantar akan menambah kepadanya.",
359
+ "access-request.text.sent": "dihantar kepada mereka yang memutuskan akses.",
360
+ "access-request.text.added to waiting": "ditambah kepada permintaan yang menunggu.",
361
+ "access-request.text.who asks": "{name} memohon",
362
+ "access-request.text.grant with role": "berikan dengan memasukkan ke dalam peranan",
363
+ "access-request.text.no role grants it": "tiada peranan yang memberikannya. tambahkannya pada peranan di skrin peranan dahulu.",
364
+ "access-request.text.privilege count": "{count} kebenaran",
365
+ "access-request.field.reason": "sebab ia diperlukan",
366
+ "access-request.field.deny reason": "sebab menolak",
367
+ "access-request.field.requester": "peminta",
368
+ "access-request.field.needed": "yang diperlukan",
369
+ "access-request.field.count": "kali",
370
+ "access-request.field.decided by": "diputuskan oleh",
371
+ "access-request.field.granted role": "peranan diberikan",
372
+ "access-request.field.decision reason": "sebab",
373
+ "access-request.field.decided at": "diputuskan pada",
374
+ "access-request.button.send": "hantar",
375
+ "access-request.button.grant": "berikan",
376
+ "access-request.button.deny": "tolak",
377
+ "access-request.button.decide": "putuskan",
378
+ "access-request.button.withdraw": "tarik balik permintaan",
379
+ "access-request.state.pending": "menunggu",
380
+ "access-request.state.granted": "diberikan",
381
+ "access-request.state.denied": "ditolak",
382
+ "access-request.state.withdrawn": "ditarik balik",
383
+ "access-request.state.expired": "tamat tempoh",
384
+ "access-request.issue.access-request-not-found": "permintaan tidak ditemui dalam $t(term.domain) ini.",
385
+ "access-request.issue.request-not-pending": "permintaan ini sudah {state}.",
386
+ "access-request.issue.own-request": "anda tidak boleh memutuskan permintaan sendiri.",
387
+ "access-request.issue.requester-unavailable": "peminta dikunci, tidak aktif atau tiada lagi dalam $t(term.domain) ini.",
388
+ "access-request.issue.deny-needs-reason": "tulis sebab penolakan.",
389
+ "access-request.issue.already-holds": "peminta sudah memilikinya melalui peranan lain.",
390
+ "access-request.issue.role-required": "pilih peranan.",
391
+ "access-request.issue.role-not-assignable": "$t(term.domain) ini tidak boleh memberikan peranan itu.",
392
+ "access-request.issue.role-does-not-grant": "{role} tidak memegang {privilege}.",
393
+ "access-request.issue.role-grants-more": "{role} juga memberikan {count} lagi: {privileges}",
394
+ "access-request.issue.role-grants-admin": "{role} membolehkan mereka memberikan peranan kepada orang lain.",
395
+ "access-request.issue.smaller-role-exists": "peranan lebih kecil juga memberikannya: {role} ({count} kebenaran)",
396
+ "access-request.issue.decider-lacks-it": "anda memberikan apa yang anda sendiri tiada: {privileges}",
397
+ "access-request.issue.denied-before": "pernah ditolak oleh {by} ({at}): {reason}",
398
+ "access-request.title.decision draft": "draf keputusan akses",
399
+ "access-request.title.request draft": "draf permintaan akses",
400
+ "access-request.field.request": "permintaan",
401
+ "access-request.field.decision": "keputusan",
402
+ "access-request.field.category": "kategori",
403
+ "access-request.field.privilege": "kebenaran",
404
+ "access-request.decision.grant": "berikan",
405
+ "access-request.decision.deny": "tolak",
406
+ "access-request.issue.not-by-role": "peranan tidak akan membukanya di sini ({because}).",
407
+ "access-request.dock.mode": "permintaan akses",
408
+ "access-request.dock.placeholder": "cth: susun permintaan akses hari ini",
409
+ "access-request.text.recorded and told": "permintaan ini direkodkan dan {count} orang yang memutuskan akses akan dimaklumkan.",
410
+ "access-request.text.nobody to tell": "tiada sesiapa dalam $t(term.domain) ini yang memutuskan akses, jadi ia tidak boleh dihantar. hubungi pentadbir anda terus.",
411
+ "access-request.text.denied lately": "ditolak pada {at}: {reason}. untuk memohon semula, tulis apa yang telah berubah.",
412
+ "access-request.button.request": "mohon akses…"
353
413
  }
@@ -349,5 +349,65 @@
349
349
  "text.x on": "{x} 个开启",
350
350
  "text.x people hold this role": "拥有此角色的人:{x}",
351
351
  "title.add domain owner": "添加$t(term.domain)所有者",
352
- "title.role privileges": "角色权限"
352
+ "title.role privileges": "角色权限",
353
+ "access-request.title.access requests": "权限请求",
354
+ "access-request.title.my access requests": "我的权限请求",
355
+ "access-request.title.decide": "处理权限请求",
356
+ "access-request.title.request access": "请求权限",
357
+ "access-request.text.refused at": "被拒绝的操作: {operation}",
358
+ "access-request.text.already waiting": "已有待处理的请求(最后 {at})。再次发送会合并到该请求。",
359
+ "access-request.text.sent": "已发送给负责权限的人。",
360
+ "access-request.text.added to waiting": "已合并到待处理的请求。",
361
+ "access-request.text.who asks": "{name} 的请求",
362
+ "access-request.text.grant with role": "加入角色以授予",
363
+ "access-request.text.no role grants it": "没有角色包含此权限。请先在角色页面将其加入角色。",
364
+ "access-request.text.privilege count": "{count} 项权限",
365
+ "access-request.field.reason": "需要的理由",
366
+ "access-request.field.deny reason": "拒绝理由",
367
+ "access-request.field.requester": "请求者",
368
+ "access-request.field.needed": "所需权限",
369
+ "access-request.field.count": "次数",
370
+ "access-request.field.decided by": "处理人",
371
+ "access-request.field.granted role": "授予的角色",
372
+ "access-request.field.decision reason": "理由",
373
+ "access-request.field.decided at": "处理时间",
374
+ "access-request.button.send": "发送",
375
+ "access-request.button.grant": "授予",
376
+ "access-request.button.deny": "拒绝",
377
+ "access-request.button.decide": "处理",
378
+ "access-request.button.withdraw": "撤回请求",
379
+ "access-request.state.pending": "待处理",
380
+ "access-request.state.granted": "已授予",
381
+ "access-request.state.denied": "已拒绝",
382
+ "access-request.state.withdrawn": "已撤回",
383
+ "access-request.state.expired": "已过期",
384
+ "access-request.issue.access-request-not-found": "在此$t(term.domain)中找不到该请求。",
385
+ "access-request.issue.request-not-pending": "该请求已{state}。",
386
+ "access-request.issue.own-request": "不能处理自己的请求。",
387
+ "access-request.issue.requester-unavailable": "请求者已被锁定、未激活或不在此$t(term.domain)中。",
388
+ "access-request.issue.deny-needs-reason": "请填写拒绝理由。",
389
+ "access-request.issue.already-holds": "请求者已通过其他角色拥有该权限。",
390
+ "access-request.issue.role-required": "请选择角色。",
391
+ "access-request.issue.role-not-assignable": "此$t(term.domain)不能分配该角色。",
392
+ "access-request.issue.role-does-not-grant": "{role} 不包含 {privilege} 权限。",
393
+ "access-request.issue.role-grants-more": "{role} 还会授予另外 {count} 项: {privileges}",
394
+ "access-request.issue.role-grants-admin": "获得 {role} 后可以给他人授予角色。",
395
+ "access-request.issue.smaller-role-exists": "更小的角色也包含此权限: {role}({count} 项权限)",
396
+ "access-request.issue.decider-lacks-it": "您将授予自己没有的权限: {privileges}",
397
+ "access-request.issue.denied-before": "之前被拒绝过 — {by},{at}: {reason}",
398
+ "access-request.title.decision draft": "权限请求处理草案",
399
+ "access-request.title.request draft": "权限请求草案",
400
+ "access-request.field.request": "请求",
401
+ "access-request.field.decision": "处理",
402
+ "access-request.field.category": "类别",
403
+ "access-request.field.privilege": "权限",
404
+ "access-request.decision.grant": "授予",
405
+ "access-request.decision.deny": "拒绝",
406
+ "access-request.issue.not-by-role": "此权限在此无法通过角色开放({because})。",
407
+ "access-request.dock.mode": "权限请求",
408
+ "access-request.dock.placeholder": "例如: 整理今天的权限请求",
409
+ "access-request.text.recorded and told": "此请求会被记录,并通知 {count} 位负责权限的管理员。",
410
+ "access-request.text.nobody to tell": "此$t(term.domain)没有负责权限的管理员,无法发送。请直接联系管理员。",
411
+ "access-request.text.denied lately": "已于 {at} 被拒绝: {reason}。如需再次请求,请写明有何变化。",
412
+ "access-request.button.request": "请求权限…"
353
413
  }