@things-factory/auth-ui 10.1.111 → 10.1.116

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.
@@ -0,0 +1,17 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ const source = (path) => readFileSync(join(__dirname, '..', path), 'utf8');
4
+ it('requesters can find their history without waiting for a result notification', () => {
5
+ const page = source('pages/profile.ts');
6
+ expect(page).toContain("navigate('my-access-requests')");
7
+ expect(page).toContain("i18next.t('access-request.title.my requests')");
8
+ });
9
+ it('user management links to the decision list, whose server door remains guarded', () => {
10
+ const page = source('pages/user/user-list-page.ts');
11
+ expect(page).toContain("navigate('access-requests')");
12
+ expect(page).toContain("i18next.t('access-request.title.access requests')");
13
+ const query = readFileSync(join(__dirname, '../../../auth-base/server/service/access-request/access-request-query.ts'), 'utf8');
14
+ expect(query).toMatch(/@privilege\(category: "user", privilege: "mutation", domainOwnerGranted: true\)/);
15
+ expect(query).toMatch(/@Directive\(DECIDE\)\s+@Query\([^\n]+\)\s+async accessRequests\(/);
16
+ });
17
+ //# sourceMappingURL=access-request-navigation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-request-navigation.test.js","sourceRoot":"","sources":["../../client/utils/access-request-navigation.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;AAElF,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;IACrF,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;IACvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gCAAgC,CAAC,CAAA;IACxD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,+CAA+C,CAAC,CAAA;AACzE,CAAC,CAAC,CAAA;AAEF,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;IACvF,MAAM,IAAI,GAAG,MAAM,CAAC,8BAA8B,CAAC,CAAA;IACnD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAA;IACrD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,mDAAmD,CAAC,CAAA;IAC3E,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,0EAA0E,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/H,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,iFAAiF,CAAC,CAAA;IACxG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,kEAAkE,CAAC,CAAA;AAC3F,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\nconst source = (path: string) => readFileSync(join(__dirname, '..', path), 'utf8')\n\nit('requesters can find their history without waiting for a result notification', () => {\n const page = source('pages/profile.ts')\n expect(page).toContain(\"navigate('my-access-requests')\")\n expect(page).toContain(\"i18next.t('access-request.title.my requests')\")\n})\n\nit('user management links to the decision list, whose server door remains guarded', () => {\n const page = source('pages/user/user-list-page.ts')\n expect(page).toContain(\"navigate('access-requests')\")\n expect(page).toContain(\"i18next.t('access-request.title.access requests')\")\n const query = readFileSync(join(__dirname, '../../../auth-base/server/service/access-request/access-request-query.ts'), 'utf8')\n expect(query).toMatch(/@privilege\\(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true\\)/)\n expect(query).toMatch(/@Directive\\(DECIDE\\)\\s+@Query\\([^\\n]+\\)\\s+async accessRequests\\(/)\n})\n"]}
@@ -20,6 +20,15 @@ export type ReviewIssue = {
20
20
  field?: string;
21
21
  params?: Record<string, unknown>;
22
22
  };
23
+ /** How many privileges a finding names before it says "and N more" - a role can carry sixty. */
24
+ export declare const PRIVILEGES_NAMED = 5;
25
+ /**
26
+ * The findings in the order a decider should read them: what blocks, then what hands on power, then the rest.
27
+ * A long list of extra privileges must not push "they could give roles to others" out of sight.
28
+ */
29
+ export declare function orderedIssues<T extends ReviewIssue>(issues: T[]): T[];
30
+ /** "a, b, c, d, e and 53 more" - the full list stays in the server's params. */
31
+ export declare function shortList(t: Translate, csv: string, shown?: number): string;
23
32
  /** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */
24
33
  export declare function issueWords(t: Translate, issue: ReviewIssue): string;
25
34
  export declare const blocks: (issues: ReviewIssue[]) => boolean;
@@ -29,9 +29,28 @@ export function requestableFrom(extensions, operationName) {
29
29
  export function privilegeWords(t, category, privilege) {
30
30
  return t(`privilege.allows.${category}.${privilege}`, { defaultValue: '' }) || `${category}:${privilege}`;
31
31
  }
32
+ /** How many privileges a finding names before it says "and N more" - a role can carry sixty. */
33
+ export const PRIVILEGES_NAMED = 5;
34
+ /**
35
+ * The findings in the order a decider should read them: what blocks, then what hands on power, then the rest.
36
+ * A long list of extra privileges must not push "they could give roles to others" out of sight.
37
+ */
38
+ export function orderedIssues(issues) {
39
+ const weight = (issue) => (issue.blocking ? 0 : issue.code === 'role-grants-admin' ? 1 : issue.code === 'smaller-role-exists' ? 2 : 3);
40
+ return [...issues].sort((a, b) => weight(a) - weight(b));
41
+ }
42
+ /** "a, b, c, d, e and 53 more" - the full list stays in the server's params. */
43
+ export function shortList(t, csv, shown = PRIVILEGES_NAMED) {
44
+ const all = String(csv || '').split(',').map(x => x.trim()).filter(Boolean);
45
+ if (all.length <= shown)
46
+ return all.join(', ');
47
+ return t('access-request.text.and more', { list: all.slice(0, shown).join(', '), count: all.length - shown, defaultValue: `${all.slice(0, shown).join(', ')} +${all.length - shown}` });
48
+ }
32
49
  /** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */
33
50
  export function issueWords(t, issue) {
34
51
  const params = { ...(issue.params || {}) };
52
+ if (typeof params.privileges === 'string')
53
+ params.privileges = shortList(t, params.privileges);
35
54
  if (typeof params.state === 'string')
36
55
  params.state = t(`access-request.state.${String(params.state).toLowerCase()}`, { defaultValue: String(params.state) });
37
56
  return t(`access-request.issue.${issue.code}`, { ...params, defaultValue: issue.code });
@@ -1 +1 @@
1
- {"version":3,"file":"access-request.js","sourceRoot":"","sources":["../../client/utils/access-request.ts"],"names":[],"mappings":"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","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"]}
1
+ {"version":3,"file":"access-request.js","sourceRoot":"","sources":["../../client/utils/access-request.ts"],"names":[],"mappings":"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","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/** How many privileges a finding names before it says \"and N more\" - a role can carry sixty. */\nexport const PRIVILEGES_NAMED = 5\n\n/**\n * The findings in the order a decider should read them: what blocks, then what hands on power, then the rest.\n * A long list of extra privileges must not push \"they could give roles to others\" out of sight.\n */\nexport function orderedIssues<T extends ReviewIssue>(issues: T[]): T[] {\n const weight = (issue: T) => (issue.blocking ? 0 : issue.code === 'role-grants-admin' ? 1 : issue.code === 'smaller-role-exists' ? 2 : 3)\n return [...issues].sort((a, b) => weight(a) - weight(b))\n}\n\n/** \"a, b, c, d, e and 53 more\" - the full list stays in the server's params. */\nexport function shortList(t: Translate, csv: string, shown = PRIVILEGES_NAMED): string {\n const all = String(csv || '').split(',').map(x => x.trim()).filter(Boolean)\n if (all.length <= shown) return all.join(', ')\n return t('access-request.text.and more', { list: all.slice(0, shown).join(', '), count: all.length - shown, defaultValue: `${all.slice(0, shown).join(', ')} +${all.length - shown}` })\n}\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.privileges === 'string') params.privileges = shortList(t, params.privileges)\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"]}
@@ -1,4 +1,4 @@
1
- import { blocks, decisionButtons, firstCandidate, issueWords, maySend, privilegeWords, recentDenial, requestableFrom } from './access-request';
1
+ import { blocks, decisionButtons, firstCandidate, issueWords, maySend, orderedIssues, privilegeWords, recentDenial, requestableFrom, shortList } from './access-request';
2
2
  /**
3
3
  * The access request on the screen (ADR-0103). The refusal says why; only a missing role offers a request.
4
4
  */
@@ -67,4 +67,20 @@ describe('asking is not a reflex (Q9)', () => {
67
67
  expect(maySend('moved to inbound', { reason: 'inbound' })).toBe(true);
68
68
  });
69
69
  });
70
+ describe('the decision popup reads well with a big role (found end to end, 2026-10-02)', () => {
71
+ const t = (key, options = {}) => (key === 'access-request.text.and more' ? `${options.list} +${options.count}` : options.defaultValue ?? key);
72
+ it('★ what blocks first, then the power to give roles, then a smaller role, then the rest', () => {
73
+ const issues = [
74
+ { code: 'role-grants-more', blocking: false },
75
+ { code: 'smaller-role-exists', blocking: false },
76
+ { code: 'role-grants-admin', blocking: false },
77
+ { code: 'deny-needs-reason', blocking: true }
78
+ ];
79
+ expect(orderedIssues(issues).map(i => i.code)).toEqual(['deny-needs-reason', 'role-grants-admin', 'smaller-role-exists', 'role-grants-more']);
80
+ });
81
+ it('★ a long privilege list names five and counts the rest', () => {
82
+ expect(shortList(t, 'a, b, c, d, e, f, g')).toBe('a, b, c, d, e +2');
83
+ expect(shortList(t, 'a, b')).toBe('a, b');
84
+ });
85
+ });
70
86
  //# sourceMappingURL=access-request.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"access-request.test.js","sourceRoot":"","sources":["../../client/utils/access-request.test.ts"],"names":[],"mappings":"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","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"]}
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,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAExK;;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;AAEF,QAAQ,CAAC,8EAA8E,EAAE,GAAG,EAAE;IAC5F,MAAM,CAAC,GAAG,CAAC,GAAW,EAAE,UAAe,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,8BAA8B,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,IAAI,GAAG,CAAC,CAAA;IAE1J,EAAE,CAAC,uFAAuF,EAAE,GAAG,EAAE;QAC/F,MAAM,MAAM,GAAG;YACb,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE;YAC7C,EAAE,IAAI,EAAE,qBAAqB,EAAE,QAAQ,EAAE,KAAK,EAAE;YAChD,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,KAAK,EAAE;YAC9C,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC9C,CAAA;QACD,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,mBAAmB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAC/I,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;QACpE,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { blocks, decisionButtons, firstCandidate, issueWords, maySend, orderedIssues, privilegeWords, recentDenial, requestableFrom, shortList } 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\ndescribe('the decision popup reads well with a big role (found end to end, 2026-10-02)', () => {\n const t = (key: string, options: any = {}) => (key === 'access-request.text.and more' ? `${options.list} +${options.count}` : options.defaultValue ?? key)\n\n it('★ what blocks first, then the power to give roles, then a smaller role, then the rest', () => {\n const issues = [\n { code: 'role-grants-more', blocking: false },\n { code: 'smaller-role-exists', blocking: false },\n { code: 'role-grants-admin', blocking: false },\n { code: 'deny-needs-reason', blocking: true }\n ]\n expect(orderedIssues(issues).map(i => i.code)).toEqual(['deny-needs-reason', 'role-grants-admin', 'smaller-role-exists', 'role-grants-more'])\n })\n\n it('★ a long privilege list names five and counts the rest', () => {\n expect(shortList(t, 'a, b, c, d, e, f, g')).toBe('a, b, c, d, e +2')\n expect(shortList(t, 'a, b')).toBe('a, b')\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-ui",
3
- "version": "10.1.111",
3
+ "version": "10.1.116",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -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.111",
43
- "@things-factory/component-ui": "^10.1.111",
42
+ "@things-factory/auth-base": "^10.1.116",
43
+ "@things-factory/component-ui": "^10.1.116",
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": "f33122c15e5b3e45393129e9418e2c367a273ade"
48
+ "gitHead": "49fa935cc703be70200eef4d29f473de98f871ce"
49
49
  }
@@ -411,5 +411,12 @@
411
411
  "access-request.text.denied lately": "denied on {at}: {reason}. to ask again, write what has changed.",
412
412
  "access-request.button.request": "request access…",
413
413
  "text.parent roles are changed by the parent domain": "roles of the parent domain can be changed only by someone who decides roles there.",
414
- "access-request.issue.role-belongs-to-parent": "{role} belongs to the parent domain - only someone who decides roles there can give it."
414
+ "access-request.issue.role-belongs-to-parent": "{role} belongs to the parent domain - only someone who decides roles there can give it.",
415
+ "access-request.dock.intro title": "decide access requests with the assistant",
416
+ "access-request.dock.intro subtitle": "it reads the waiting requests and drafts decisions; you press the card.",
417
+ "access-request.dock.examples": "examples",
418
+ "access-request.dock.example today": "sort out today's access requests",
419
+ "access-request.dock.example smallest": "grant each with the smallest role that gives it",
420
+ "access-request.dock.example one person": "show what one person asked for and what each role would give",
421
+ "access-request.text.and more": "{list} and {count} more"
415
422
  }
@@ -411,5 +411,12 @@
411
411
  "access-request.text.denied lately": "{at} に拒否されました: {reason}。再度リクエストするには、何が変わったかを書いてください。",
412
412
  "access-request.button.request": "権限をリクエスト…",
413
413
  "text.parent roles are changed by the parent domain": "上位$t(term.domain)のロールは、その$t(term.domain)で権限を決める人だけが変更できます。",
414
- "access-request.issue.role-belongs-to-parent": "{role} は上位$t(term.domain)のロールです。その$t(term.domain)で権限を決める人だけが付与できます。"
414
+ "access-request.issue.role-belongs-to-parent": "{role} は上位$t(term.domain)のロールです。その$t(term.domain)で権限を決める人だけが付与できます。",
415
+ "access-request.dock.intro title": "AI と一緒に権限リクエストを整理しましょう",
416
+ "access-request.dock.intro subtitle": "保留中のリクエストを読んで決定案を作ります。決定はカードで押します。",
417
+ "access-request.dock.examples": "例",
418
+ "access-request.dock.example today": "今日届いた権限リクエストを整理して",
419
+ "access-request.dock.example smallest": "リクエストごとに最小のロールで付与案を作って",
420
+ "access-request.dock.example one person": "一人のリクエストと、ロールごとに追加で与えるものを見せて",
421
+ "access-request.text.and more": "{list} ほか {count} 件"
415
422
  }
@@ -411,5 +411,12 @@
411
411
  "access-request.text.denied lately": "{at}에 거부되었습니다: {reason}. 다시 요청하려면 무엇이 달라졌는지 적어 주세요.",
412
412
  "access-request.button.request": "권한 요청…",
413
413
  "text.parent roles are changed by the parent domain": "상위 $t(term.domain)의 역할은 그 $t(term.domain)에서 권한을 정하는 사람만 바꿀 수 있습니다.",
414
- "access-request.issue.role-belongs-to-parent": "{role} 역할은 상위 $t(term.domain)의 것입니다. 그 $t(term.domain)에서 권한을 정하는 사람만 줄 수 있습니다."
414
+ "access-request.issue.role-belongs-to-parent": "{role} 역할은 상위 $t(term.domain)의 것입니다. 그 $t(term.domain)에서 권한을 정하는 사람만 줄 수 있습니다.",
415
+ "access-request.dock.intro title": "AI 와 함께 권한 요청을 정리하세요",
416
+ "access-request.dock.intro subtitle": "기다리는 요청을 읽고 결정안을 만듭니다. 결정은 카드에서 직접 누릅니다.",
417
+ "access-request.dock.examples": "예시",
418
+ "access-request.dock.example today": "오늘 들어온 권한 요청 정리해 줘",
419
+ "access-request.dock.example smallest": "요청마다 가장 작은 역할로 부여안을 만들어 줘",
420
+ "access-request.dock.example one person": "한 사람이 요청한 것과 역할마다 더 주는 것을 보여 줘",
421
+ "access-request.text.and more": "{list} 외 {count}개"
415
422
  }
@@ -411,5 +411,12 @@
411
411
  "access-request.text.denied lately": "ditolak pada {at}: {reason}. untuk memohon semula, tulis apa yang telah berubah.",
412
412
  "access-request.button.request": "mohon akses…",
413
413
  "text.parent roles are changed by the parent domain": "peranan $t(term.domain) induk hanya boleh diubah oleh mereka yang memutuskan peranan di sana.",
414
- "access-request.issue.role-belongs-to-parent": "{role} milik $t(term.domain) induk - hanya mereka yang memutuskan peranan di sana boleh memberikannya."
414
+ "access-request.issue.role-belongs-to-parent": "{role} milik $t(term.domain) induk - hanya mereka yang memutuskan peranan di sana boleh memberikannya.",
415
+ "access-request.dock.intro title": "putuskan permintaan akses bersama pembantu",
416
+ "access-request.dock.intro subtitle": "ia membaca permintaan yang menunggu dan menyediakan draf keputusan; anda menekan kad.",
417
+ "access-request.dock.examples": "contoh",
418
+ "access-request.dock.example today": "susun permintaan akses hari ini",
419
+ "access-request.dock.example smallest": "sediakan draf pemberian dengan peranan terkecil bagi setiap permintaan",
420
+ "access-request.dock.example one person": "tunjukkan apa yang diminta seseorang dan apa yang diberi oleh setiap peranan",
421
+ "access-request.text.and more": "{list} dan {count} lagi"
415
422
  }
@@ -411,5 +411,12 @@
411
411
  "access-request.text.denied lately": "已于 {at} 被拒绝: {reason}。如需再次请求,请写明有何变化。",
412
412
  "access-request.button.request": "请求权限…",
413
413
  "text.parent roles are changed by the parent domain": "上级$t(term.domain)的角色只能由该$t(term.domain)负责权限的人更改。",
414
- "access-request.issue.role-belongs-to-parent": "{role} 属于上级$t(term.domain),只有在该$t(term.domain)负责权限的人才能授予。"
414
+ "access-request.issue.role-belongs-to-parent": "{role} 属于上级$t(term.domain),只有在该$t(term.domain)负责权限的人才能授予。",
415
+ "access-request.dock.intro title": "与 AI 一起处理权限请求",
416
+ "access-request.dock.intro subtitle": "读取待处理的请求并起草处理方案,由您在卡片上确认。",
417
+ "access-request.dock.examples": "示例",
418
+ "access-request.dock.example today": "整理今天的权限请求",
419
+ "access-request.dock.example smallest": "为每个请求用最小的角色起草授予方案",
420
+ "access-request.dock.example one person": "显示某人的请求以及每个角色额外授予的内容",
421
+ "access-request.text.and more": "{list} 等另外 {count} 项"
415
422
  }