@things-factory/auth-ui 10.1.16 → 10.1.17
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.
- package/dist-client/components/secret-field.d.ts +35 -0
- package/dist-client/components/secret-field.js +149 -0
- package/dist-client/components/secret-field.js.map +1 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.d.ts +1 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
- package/dist-client/pages/app-binding/app-binding.d.ts +1 -0
- package/dist-client/pages/app-binding/app-binding.js +8 -8
- package/dist-client/pages/app-binding/app-binding.js.map +1 -1
- package/dist-client/pages/app-binding/app-bindings.d.ts +63 -2
- package/dist-client/pages/app-binding/app-bindings.js +199 -117
- package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
- package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
- package/dist-client/pages/appliance/appliance.d.ts +1 -0
- package/dist-client/pages/appliance/appliance.js +2 -4
- package/dist-client/pages/appliance/appliance.js.map +1 -1
- package/dist-client/pages/appliance/home.d.ts +61 -2
- package/dist-client/pages/appliance/home.js +200 -122
- package/dist-client/pages/appliance/home.js.map +1 -1
- package/dist-client/pages/application/application.d.ts +1 -0
- package/dist-client/pages/application/application.js +10 -3
- package/dist-client/pages/application/application.js.map +1 -1
- package/dist-client/pages/application/applications.d.ts +61 -2
- package/dist-client/pages/application/applications.js +193 -128
- package/dist-client/pages/application/applications.js.map +1 -1
- package/dist-client/pages/application/register.js +81 -27
- package/dist-client/pages/application/register.js.map +1 -1
- package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
- package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
- package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
- package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
- package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
- package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
- package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
- package/dist-client/pages/secrets-are-not-printed.test.js +70 -0
- package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/application-status.d.ts +32 -0
- package/dist-client/utils/application-status.js +45 -0
- package/dist-client/utils/application-status.js.map +1 -0
- package/dist-client/utils/application-status.test.d.ts +1 -0
- package/dist-client/utils/application-status.test.js +45 -0
- package/dist-client/utils/application-status.test.js.map +1 -0
- package/dist-client/utils/form-values.d.ts +42 -0
- package/dist-client/utils/form-values.js +57 -0
- package/dist-client/utils/form-values.js.map +1 -0
- package/dist-client/utils/form-values.test.d.ts +1 -0
- package/dist-client/utils/form-values.test.js +62 -0
- package/dist-client/utils/form-values.test.js.map +1 -0
- package/dist-client/utils/list-result.d.ts +62 -0
- package/dist-client/utils/list-result.js +92 -0
- package/dist-client/utils/list-result.js.map +1 -0
- package/dist-client/utils/list-result.test.d.ts +1 -0
- package/dist-client/utils/list-result.test.js +160 -0
- package/dist-client/utils/list-result.test.js.map +1 -0
- package/dist-client/utils/secret-display.d.ts +51 -0
- package/dist-client/utils/secret-display.js +61 -0
- package/dist-client/utils/secret-display.js.map +1 -0
- package/dist-client/utils/secret-display.test.d.ts +1 -0
- package/dist-client/utils/secret-display.test.js +59 -0
- package/dist-client/utils/secret-display.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/translations/en.json +31 -0
- package/translations/ja.json +31 -0
- package/translations/ko.json +31 -0
- package/translations/ms.json +31 -0
- package/translations/zh.json +31 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { arrayFromResponse, listOutcome, pageFromResponse } from './list-result';
|
|
2
|
+
/**
|
|
3
|
+
* A list screen may only claim what the server actually answered.
|
|
4
|
+
*
|
|
5
|
+
* The three auth list screens each carried this rule inline and each got it wrong the same
|
|
6
|
+
* way: a failed query produced an empty table with nothing said. These cases are the ones
|
|
7
|
+
* that were failing in production, written down so they stay fixed.
|
|
8
|
+
*/
|
|
9
|
+
describe('reading a list page out of a response', () => {
|
|
10
|
+
it('reads the page when the query answered', () => {
|
|
11
|
+
const response = { data: { applications: { items: [{ id: 'a' }], total: 1 } } };
|
|
12
|
+
expect(pageFromResponse(response, 'applications')).toEqual({ items: [{ id: 'a' }], total: 1 });
|
|
13
|
+
});
|
|
14
|
+
it('refuses a response carrying errors', () => {
|
|
15
|
+
/*
|
|
16
|
+
* The shared client runs with errorPolicy 'all', so errors arrive here instead of
|
|
17
|
+
* throwing. This is the case that used to slip through.
|
|
18
|
+
*/
|
|
19
|
+
const response = { errors: [{ message: 'user:query 권한이 필요합니다' }], data: null };
|
|
20
|
+
expect(pageFromResponse(response, 'applications')).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
it('refuses a response that carries errors even when the page looks complete', () => {
|
|
23
|
+
/*
|
|
24
|
+
* ⚠ The case that makes the error check load-bearing.
|
|
25
|
+
*
|
|
26
|
+
* errorPolicy 'all' can return `data` **and** `errors` together — a list that came back
|
|
27
|
+
* while some field resolver failed. Every other case here is caught by the data checks,
|
|
28
|
+
* so without this one the error branch could be deleted and the suite stayed green. It
|
|
29
|
+
* was: removing it changed nothing until this test existed.
|
|
30
|
+
*
|
|
31
|
+
* Refusing is the lean. A partial list rendered as a whole one is a claim the screen
|
|
32
|
+
* cannot support, and the person reading it has no way to tell which rows are missing.
|
|
33
|
+
*/
|
|
34
|
+
const response = {
|
|
35
|
+
errors: [{ message: 'privileges field failed' }],
|
|
36
|
+
data: { applications: { items: [{ id: 'a' }], total: 1 } }
|
|
37
|
+
};
|
|
38
|
+
expect(pageFromResponse(response, 'applications')).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
it('refuses a partial response where the field is null but errors explain why', () => {
|
|
41
|
+
/*
|
|
42
|
+
* The quiet one. `data` exists, so a check that only asks "is data there" passes, and
|
|
43
|
+
* the next line reads `.items` off null.
|
|
44
|
+
*/
|
|
45
|
+
const response = { errors: [{ message: 'forbidden' }], data: { applications: null } };
|
|
46
|
+
expect(pageFromResponse(response, 'applications')).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
it('refuses a response with no errors but no field either', () => {
|
|
49
|
+
expect(pageFromResponse({ data: {} }, 'applications')).toBeUndefined();
|
|
50
|
+
expect(pageFromResponse({ data: { applications: null } }, 'applications')).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it('refuses a field that is not a page', () => {
|
|
53
|
+
/* `items` missing means we cannot list it, whatever else came back. */
|
|
54
|
+
expect(pageFromResponse({ data: { applications: { total: 3 } } }, 'applications')).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it('refuses nothing at all', () => {
|
|
57
|
+
expect(pageFromResponse(undefined, 'applications')).toBeUndefined();
|
|
58
|
+
expect(pageFromResponse(null, 'applications')).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('telling a refused query from a withheld column', () => {
|
|
62
|
+
/*
|
|
63
|
+
* This house gates individual columns: `Appliance.accessToken` and
|
|
64
|
+
* `AuthProvider.clientSecret` each carry their own @privilege. A viewer without it gets
|
|
65
|
+
* the whole list plus one error pointing at that value.
|
|
66
|
+
*
|
|
67
|
+
* Reading the error's path is what separates the two. Getting it wrong is visible either
|
|
68
|
+
* way: black out a list that arrived, or show a refused query as empty.
|
|
69
|
+
*/
|
|
70
|
+
it('keeps the page when only a value inside it was refused', () => {
|
|
71
|
+
const response = {
|
|
72
|
+
errors: [{ message: 'security:query 권한이 필요합니다', path: ['authProviders', 'items', 2, 'clientSecret'] }],
|
|
73
|
+
data: { authProviders: { items: [{ id: 'a', clientSecret: null }], total: 1 } }
|
|
74
|
+
};
|
|
75
|
+
expect(pageFromResponse(response, 'authProviders')).toEqual({
|
|
76
|
+
items: [{ id: 'a', clientSecret: null }],
|
|
77
|
+
total: 1
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('refuses when the query itself was the thing rejected', () => {
|
|
81
|
+
const response = {
|
|
82
|
+
errors: [{ message: 'user:query 권한이 필요합니다', path: ['authProviders'] }],
|
|
83
|
+
data: { authProviders: null }
|
|
84
|
+
};
|
|
85
|
+
expect(pageFromResponse(response, 'authProviders')).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
it('refuses an error that does not say where it happened', () => {
|
|
88
|
+
/* Unknown scope leans to the safe side: a network or parse failure stops the list. */
|
|
89
|
+
const response = {
|
|
90
|
+
errors: [{ message: 'Failed to fetch' }],
|
|
91
|
+
data: { authProviders: { items: [{ id: 'a' }], total: 1 } }
|
|
92
|
+
};
|
|
93
|
+
expect(pageFromResponse(response, 'authProviders')).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
it('refuses when any one error is fatal, even beside a harmless one', () => {
|
|
96
|
+
const response = {
|
|
97
|
+
errors: [
|
|
98
|
+
{ message: 'withheld', path: ['authProviders', 'items', 0, 'clientSecret'] },
|
|
99
|
+
{ message: 'refused', path: ['authProviders'] }
|
|
100
|
+
],
|
|
101
|
+
data: { authProviders: { items: [], total: 0 } }
|
|
102
|
+
};
|
|
103
|
+
expect(pageFromResponse(response, 'authProviders')).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
it('applies the same reading to a bare array', () => {
|
|
106
|
+
const response = {
|
|
107
|
+
errors: [{ message: 'withheld', path: ['domainOwners', 0, 'reason'] }],
|
|
108
|
+
data: { domainOwners: [{ id: 'a', reason: null }] }
|
|
109
|
+
};
|
|
110
|
+
expect(arrayFromResponse(response, 'domainOwners')).toEqual({ items: [{ id: 'a', reason: null }], total: 1 });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('a query that answers with a bare array', () => {
|
|
114
|
+
it('reads the rows and counts them', () => {
|
|
115
|
+
const response = { data: { domainOwners: [{ id: 'a' }, { id: 'b' }] } };
|
|
116
|
+
expect(arrayFromResponse(response, 'domainOwners')).toEqual({ items: [{ id: 'a' }, { id: 'b' }], total: 2 });
|
|
117
|
+
});
|
|
118
|
+
it('keeps an answer of none as an answer', () => {
|
|
119
|
+
expect(arrayFromResponse({ data: { domainOwners: [] } }, 'domainOwners')).toEqual({ items: [], total: 0 });
|
|
120
|
+
});
|
|
121
|
+
it('refuses a response carrying errors even when rows came back', () => {
|
|
122
|
+
/*
|
|
123
|
+
* ⚠ The load-bearing case, same as for pages. The screen this replaces wrote
|
|
124
|
+
* `response.data?.domainOwners || []`, so a refused query read as "this domain has no
|
|
125
|
+
* owners" — a sentence an administrator may act on by adding one.
|
|
126
|
+
*/
|
|
127
|
+
const response = { errors: [{ message: 'forbidden' }], data: { domainOwners: [{ id: 'a' }] } };
|
|
128
|
+
expect(arrayFromResponse(response, 'domainOwners')).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
it('refuses a missing or non-array field', () => {
|
|
131
|
+
expect(arrayFromResponse({ data: {} }, 'domainOwners')).toBeUndefined();
|
|
132
|
+
expect(arrayFromResponse({ data: { domainOwners: null } }, 'domainOwners')).toBeUndefined();
|
|
133
|
+
expect(arrayFromResponse({ data: { domainOwners: { items: [] } } }, 'domainOwners')).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('what the screen may then say', () => {
|
|
137
|
+
it('shows the rows when there are rows', () => {
|
|
138
|
+
expect(listOutcome({ items: [{ id: 'a' }, { id: 'b' }], total: 2 })).toEqual({
|
|
139
|
+
items: [{ id: 'a' }, { id: 'b' }],
|
|
140
|
+
total: 2,
|
|
141
|
+
failed: false
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
it('an answer of none is not a failure', () => {
|
|
145
|
+
/* The distinction this whole file exists for, from the other side. */
|
|
146
|
+
expect(listOutcome({ items: [], total: 0 })).toEqual({ items: [], total: 0, failed: false });
|
|
147
|
+
});
|
|
148
|
+
it('no answer is a failure, and the rows are empty without claiming emptiness', () => {
|
|
149
|
+
expect(listOutcome(undefined)).toEqual({ items: [], total: 0, failed: true });
|
|
150
|
+
expect(listOutcome(null)).toEqual({ items: [], total: 0, failed: true });
|
|
151
|
+
});
|
|
152
|
+
it('counts the rows when the server sent no total', () => {
|
|
153
|
+
expect(listOutcome({ items: [{ id: 'a' }] }).total).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
it('keeps a total of zero rather than recounting it', () => {
|
|
156
|
+
/* `?? ` and not `||` — a real zero must survive, or an empty list reports its length. */
|
|
157
|
+
expect(listOutcome({ items: [], total: 0 }).total).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=list-result.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-result.test.js","sourceRoot":"","sources":["../../client/utils/list-result.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { arrayFromResponse, listOutcome, pageFromResponse } from './list-result'\n\n/**\n * A list screen may only claim what the server actually answered.\n *\n * The three auth list screens each carried this rule inline and each got it wrong the same\n * way: a failed query produced an empty table with nothing said. These cases are the ones\n * that were failing in production, written down so they stay fixed.\n */\n\ndescribe('reading a list page out of a response', () => {\n it('reads the page when the query answered', () => {\n const response = { data: { applications: { items: [{ id: 'a' }], total: 1 } } }\n\n expect(pageFromResponse(response, 'applications')).toEqual({ items: [{ id: 'a' }], total: 1 })\n })\n\n it('refuses a response carrying errors', () => {\n /*\n * The shared client runs with errorPolicy 'all', so errors arrive here instead of\n * throwing. This is the case that used to slip through.\n */\n const response = { errors: [{ message: 'user:query 권한이 필요합니다' }], data: null }\n\n expect(pageFromResponse(response, 'applications')).toBeUndefined()\n })\n\n it('refuses a response that carries errors even when the page looks complete', () => {\n /*\n * ⚠ The case that makes the error check load-bearing.\n *\n * errorPolicy 'all' can return `data` **and** `errors` together — a list that came back\n * while some field resolver failed. Every other case here is caught by the data checks,\n * so without this one the error branch could be deleted and the suite stayed green. It\n * was: removing it changed nothing until this test existed.\n *\n * Refusing is the lean. A partial list rendered as a whole one is a claim the screen\n * cannot support, and the person reading it has no way to tell which rows are missing.\n */\n const response = {\n errors: [{ message: 'privileges field failed' }],\n data: { applications: { items: [{ id: 'a' }], total: 1 } }\n }\n\n expect(pageFromResponse(response, 'applications')).toBeUndefined()\n })\n\n it('refuses a partial response where the field is null but errors explain why', () => {\n /*\n * The quiet one. `data` exists, so a check that only asks \"is data there\" passes, and\n * the next line reads `.items` off null.\n */\n const response = { errors: [{ message: 'forbidden' }], data: { applications: null } }\n\n expect(pageFromResponse(response, 'applications')).toBeUndefined()\n })\n\n it('refuses a response with no errors but no field either', () => {\n expect(pageFromResponse({ data: {} }, 'applications')).toBeUndefined()\n expect(pageFromResponse({ data: { applications: null } }, 'applications')).toBeUndefined()\n })\n\n it('refuses a field that is not a page', () => {\n /* `items` missing means we cannot list it, whatever else came back. */\n expect(pageFromResponse({ data: { applications: { total: 3 } } }, 'applications')).toBeUndefined()\n })\n\n it('refuses nothing at all', () => {\n expect(pageFromResponse(undefined, 'applications')).toBeUndefined()\n expect(pageFromResponse(null, 'applications')).toBeUndefined()\n })\n})\n\ndescribe('telling a refused query from a withheld column', () => {\n /*\n * This house gates individual columns: `Appliance.accessToken` and\n * `AuthProvider.clientSecret` each carry their own @privilege. A viewer without it gets\n * the whole list plus one error pointing at that value.\n *\n * Reading the error's path is what separates the two. Getting it wrong is visible either\n * way: black out a list that arrived, or show a refused query as empty.\n */\n it('keeps the page when only a value inside it was refused', () => {\n const response = {\n errors: [{ message: 'security:query 권한이 필요합니다', path: ['authProviders', 'items', 2, 'clientSecret'] }],\n data: { authProviders: { items: [{ id: 'a', clientSecret: null }], total: 1 } }\n }\n\n expect(pageFromResponse(response, 'authProviders')).toEqual({\n items: [{ id: 'a', clientSecret: null }],\n total: 1\n })\n })\n\n it('refuses when the query itself was the thing rejected', () => {\n const response = {\n errors: [{ message: 'user:query 권한이 필요합니다', path: ['authProviders'] }],\n data: { authProviders: null }\n }\n\n expect(pageFromResponse(response, 'authProviders')).toBeUndefined()\n })\n\n it('refuses an error that does not say where it happened', () => {\n /* Unknown scope leans to the safe side: a network or parse failure stops the list. */\n const response = {\n errors: [{ message: 'Failed to fetch' }],\n data: { authProviders: { items: [{ id: 'a' }], total: 1 } }\n }\n\n expect(pageFromResponse(response, 'authProviders')).toBeUndefined()\n })\n\n it('refuses when any one error is fatal, even beside a harmless one', () => {\n const response = {\n errors: [\n { message: 'withheld', path: ['authProviders', 'items', 0, 'clientSecret'] },\n { message: 'refused', path: ['authProviders'] }\n ],\n data: { authProviders: { items: [], total: 0 } }\n }\n\n expect(pageFromResponse(response, 'authProviders')).toBeUndefined()\n })\n\n it('applies the same reading to a bare array', () => {\n const response = {\n errors: [{ message: 'withheld', path: ['domainOwners', 0, 'reason'] }],\n data: { domainOwners: [{ id: 'a', reason: null }] }\n }\n\n expect(arrayFromResponse(response, 'domainOwners')).toEqual({ items: [{ id: 'a', reason: null }], total: 1 })\n })\n})\n\ndescribe('a query that answers with a bare array', () => {\n it('reads the rows and counts them', () => {\n const response = { data: { domainOwners: [{ id: 'a' }, { id: 'b' }] } }\n\n expect(arrayFromResponse(response, 'domainOwners')).toEqual({ items: [{ id: 'a' }, { id: 'b' }], total: 2 })\n })\n\n it('keeps an answer of none as an answer', () => {\n expect(arrayFromResponse({ data: { domainOwners: [] } }, 'domainOwners')).toEqual({ items: [], total: 0 })\n })\n\n it('refuses a response carrying errors even when rows came back', () => {\n /*\n * ⚠ The load-bearing case, same as for pages. The screen this replaces wrote\n * `response.data?.domainOwners || []`, so a refused query read as \"this domain has no\n * owners\" — a sentence an administrator may act on by adding one.\n */\n const response = { errors: [{ message: 'forbidden' }], data: { domainOwners: [{ id: 'a' }] } }\n\n expect(arrayFromResponse(response, 'domainOwners')).toBeUndefined()\n })\n\n it('refuses a missing or non-array field', () => {\n expect(arrayFromResponse({ data: {} }, 'domainOwners')).toBeUndefined()\n expect(arrayFromResponse({ data: { domainOwners: null } }, 'domainOwners')).toBeUndefined()\n expect(arrayFromResponse({ data: { domainOwners: { items: [] } } }, 'domainOwners')).toBeUndefined()\n })\n})\n\ndescribe('what the screen may then say', () => {\n it('shows the rows when there are rows', () => {\n expect(listOutcome({ items: [{ id: 'a' }, { id: 'b' }], total: 2 })).toEqual({\n items: [{ id: 'a' }, { id: 'b' }],\n total: 2,\n failed: false\n })\n })\n\n it('an answer of none is not a failure', () => {\n /* The distinction this whole file exists for, from the other side. */\n expect(listOutcome({ items: [], total: 0 })).toEqual({ items: [], total: 0, failed: false })\n })\n\n it('no answer is a failure, and the rows are empty without claiming emptiness', () => {\n expect(listOutcome(undefined)).toEqual({ items: [], total: 0, failed: true })\n expect(listOutcome(null)).toEqual({ items: [], total: 0, failed: true })\n })\n\n it('counts the rows when the server sent no total', () => {\n expect(listOutcome({ items: [{ id: 'a' }] }).total).toBe(1)\n })\n\n it('keeps a total of zero rather than recounting it', () => {\n /* `?? ` and not `||` — a real zero must survive, or an empty list reports its length. */\n expect(listOutcome({ items: [], total: 0 }).total).toBe(0)\n })\n})\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Showing a credential on screen.
|
|
3
|
+
*
|
|
4
|
+
* ── The screens this is for ──────────────────────────────────────────────────
|
|
5
|
+
* An application's refresh token, an appliance's access token, a provider's client secret.
|
|
6
|
+
* They have one thing in common that ordinary fields do not: the reader is meant to **copy**
|
|
7
|
+
* one, once, and never read it again. Every other moment the screen is open, the value on it
|
|
8
|
+
* is a liability — on a shared display, in a screenshot, over a shoulder.
|
|
9
|
+
*
|
|
10
|
+
* Today they are `<input readonly>` with the value in plain text, shown the moment the page
|
|
11
|
+
* loads.
|
|
12
|
+
*
|
|
13
|
+
* ── The rule that has teeth: the mask must not leak the length ───────────────
|
|
14
|
+
* The obvious mask is one dot per character, and it is wrong. A token's length says which
|
|
15
|
+
* kind of token it is, and for a short secret it narrows the guess badly — a six-character
|
|
16
|
+
* mask beside a twenty-character one tells you which is worth attacking. A mask is supposed
|
|
17
|
+
* to say "there is a value here", nothing else, so it is a fixed width.
|
|
18
|
+
*
|
|
19
|
+
* The one thing a mask must distinguish is **present** from **absent**. A field with no
|
|
20
|
+
* secret shows nothing at all: dots over an empty value would have the reader hunting for a
|
|
21
|
+
* token that was never issued.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* How wide a mask is, whatever it hides.
|
|
25
|
+
*
|
|
26
|
+
* Sixteen, to match `OxGristRendererSecret` in @operato/data-grist. The platform reached the
|
|
27
|
+
* same two rules on its own — a fixed width, and nothing at all for an absent value — and two
|
|
28
|
+
* different mask widths on adjacent screens would read as two different kinds of secret.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
31
|
+
/**
|
|
32
|
+
* What to show when the value is hidden.
|
|
33
|
+
*
|
|
34
|
+
* Empty for an absent secret — see above; the distinction between "hidden" and "not issued"
|
|
35
|
+
* is the one thing the mask is allowed to carry.
|
|
36
|
+
*/
|
|
37
|
+
export declare function maskedSecret(value: string | null | undefined): string;
|
|
38
|
+
/**
|
|
39
|
+
* What to show, given whether the reader has asked to see it.
|
|
40
|
+
*
|
|
41
|
+
* Revealing is an action the reader takes, so it is never the initial state — see
|
|
42
|
+
* `secret-display.test.ts` for the case that pins it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shownSecret(value: string | null | undefined, revealed: boolean): string;
|
|
45
|
+
/**
|
|
46
|
+
* Is there anything to copy or reveal?
|
|
47
|
+
*
|
|
48
|
+
* Buttons that do nothing are worse than absent ones: the reader presses, nothing happens,
|
|
49
|
+
* and they cannot tell whether the screen is broken or the token is missing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hasSecret(value: string | null | undefined): boolean;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Showing a credential on screen.
|
|
3
|
+
*
|
|
4
|
+
* ── The screens this is for ──────────────────────────────────────────────────
|
|
5
|
+
* An application's refresh token, an appliance's access token, a provider's client secret.
|
|
6
|
+
* They have one thing in common that ordinary fields do not: the reader is meant to **copy**
|
|
7
|
+
* one, once, and never read it again. Every other moment the screen is open, the value on it
|
|
8
|
+
* is a liability — on a shared display, in a screenshot, over a shoulder.
|
|
9
|
+
*
|
|
10
|
+
* Today they are `<input readonly>` with the value in plain text, shown the moment the page
|
|
11
|
+
* loads.
|
|
12
|
+
*
|
|
13
|
+
* ── The rule that has teeth: the mask must not leak the length ───────────────
|
|
14
|
+
* The obvious mask is one dot per character, and it is wrong. A token's length says which
|
|
15
|
+
* kind of token it is, and for a short secret it narrows the guess badly — a six-character
|
|
16
|
+
* mask beside a twenty-character one tells you which is worth attacking. A mask is supposed
|
|
17
|
+
* to say "there is a value here", nothing else, so it is a fixed width.
|
|
18
|
+
*
|
|
19
|
+
* The one thing a mask must distinguish is **present** from **absent**. A field with no
|
|
20
|
+
* secret shows nothing at all: dots over an empty value would have the reader hunting for a
|
|
21
|
+
* token that was never issued.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* How wide a mask is, whatever it hides.
|
|
25
|
+
*
|
|
26
|
+
* Sixteen, to match `OxGristRendererSecret` in @operato/data-grist. The platform reached the
|
|
27
|
+
* same two rules on its own — a fixed width, and nothing at all for an absent value — and two
|
|
28
|
+
* different mask widths on adjacent screens would read as two different kinds of secret.
|
|
29
|
+
*/
|
|
30
|
+
export const MASK = '••••••••••••••••';
|
|
31
|
+
/**
|
|
32
|
+
* What to show when the value is hidden.
|
|
33
|
+
*
|
|
34
|
+
* Empty for an absent secret — see above; the distinction between "hidden" and "not issued"
|
|
35
|
+
* is the one thing the mask is allowed to carry.
|
|
36
|
+
*/
|
|
37
|
+
export function maskedSecret(value) {
|
|
38
|
+
return value ? MASK : '';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What to show, given whether the reader has asked to see it.
|
|
42
|
+
*
|
|
43
|
+
* Revealing is an action the reader takes, so it is never the initial state — see
|
|
44
|
+
* `secret-display.test.ts` for the case that pins it.
|
|
45
|
+
*/
|
|
46
|
+
export function shownSecret(value, revealed) {
|
|
47
|
+
if (!value) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
return revealed ? value : maskedSecret(value);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Is there anything to copy or reveal?
|
|
54
|
+
*
|
|
55
|
+
* Buttons that do nothing are worse than absent ones: the reader presses, nothing happens,
|
|
56
|
+
* and they cannot tell whether the screen is broken or the token is missing.
|
|
57
|
+
*/
|
|
58
|
+
export function hasSecret(value) {
|
|
59
|
+
return !!value;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=secret-display.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-display.js","sourceRoot":"","sources":["../../client/utils/secret-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,kBAAkB,CAAA;AAEtC;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAgC;IAC3D,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgC,EAAE,QAAiB;IAC7E,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;AAC/C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAgC;IACxD,OAAO,CAAC,CAAC,KAAK,CAAA;AAChB,CAAC","sourcesContent":["/**\n * Showing a credential on screen.\n *\n * ── The screens this is for ──────────────────────────────────────────────────\n * An application's refresh token, an appliance's access token, a provider's client secret.\n * They have one thing in common that ordinary fields do not: the reader is meant to **copy**\n * one, once, and never read it again. Every other moment the screen is open, the value on it\n * is a liability — on a shared display, in a screenshot, over a shoulder.\n *\n * Today they are `<input readonly>` with the value in plain text, shown the moment the page\n * loads.\n *\n * ── The rule that has teeth: the mask must not leak the length ───────────────\n * The obvious mask is one dot per character, and it is wrong. A token's length says which\n * kind of token it is, and for a short secret it narrows the guess badly — a six-character\n * mask beside a twenty-character one tells you which is worth attacking. A mask is supposed\n * to say \"there is a value here\", nothing else, so it is a fixed width.\n *\n * The one thing a mask must distinguish is **present** from **absent**. A field with no\n * secret shows nothing at all: dots over an empty value would have the reader hunting for a\n * token that was never issued.\n */\n\n/**\n * How wide a mask is, whatever it hides.\n *\n * Sixteen, to match `OxGristRendererSecret` in @operato/data-grist. The platform reached the\n * same two rules on its own — a fixed width, and nothing at all for an absent value — and two\n * different mask widths on adjacent screens would read as two different kinds of secret.\n */\nexport const MASK = '••••••••••••••••'\n\n/**\n * What to show when the value is hidden.\n *\n * Empty for an absent secret — see above; the distinction between \"hidden\" and \"not issued\"\n * is the one thing the mask is allowed to carry.\n */\nexport function maskedSecret(value: string | null | undefined): string {\n return value ? MASK : ''\n}\n\n/**\n * What to show, given whether the reader has asked to see it.\n *\n * Revealing is an action the reader takes, so it is never the initial state — see\n * `secret-display.test.ts` for the case that pins it.\n */\nexport function shownSecret(value: string | null | undefined, revealed: boolean): string {\n if (!value) {\n return ''\n }\n\n return revealed ? value : maskedSecret(value)\n}\n\n/**\n * Is there anything to copy or reveal?\n *\n * Buttons that do nothing are worse than absent ones: the reader presses, nothing happens,\n * and they cannot tell whether the screen is broken or the token is missing.\n */\nexport function hasSecret(value: string | null | undefined): boolean {\n return !!value\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { hasSecret, MASK, maskedSecret, shownSecret } from './secret-display';
|
|
2
|
+
/**
|
|
3
|
+
* The app binding and appliance detail screens print a refresh token and an access token
|
|
4
|
+
* into a readonly input, in plain text, from the moment the page opens. These are the rules
|
|
5
|
+
* that replace that.
|
|
6
|
+
*/
|
|
7
|
+
describe('the mask', () => {
|
|
8
|
+
it('is the same width whatever it hides', () => {
|
|
9
|
+
/*
|
|
10
|
+
* ⚠ The rule worth testing, because one-dot-per-character is the obvious mask and it
|
|
11
|
+
* leaks. A token's length says which kind of token it is; two masked fields of
|
|
12
|
+
* different widths tell a reader which is the short one.
|
|
13
|
+
*/
|
|
14
|
+
const short = maskedSecret('abc');
|
|
15
|
+
const long = maskedSecret('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abcdefg');
|
|
16
|
+
expect(short).toBe(long);
|
|
17
|
+
expect(short.length).not.toBe(3);
|
|
18
|
+
});
|
|
19
|
+
it('says a secret is there', () => {
|
|
20
|
+
expect(maskedSecret('anything')).toBe(MASK);
|
|
21
|
+
});
|
|
22
|
+
it('says nothing when there is no secret', () => {
|
|
23
|
+
/*
|
|
24
|
+
* Dots over an empty value send the reader hunting for a token nobody issued. The mask
|
|
25
|
+
* is allowed to carry exactly one bit — present or absent — and this is it.
|
|
26
|
+
*/
|
|
27
|
+
expect(maskedSecret('')).toBe('');
|
|
28
|
+
expect(maskedSecret(null)).toBe('');
|
|
29
|
+
expect(maskedSecret(undefined)).toBe('');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('what the field shows', () => {
|
|
33
|
+
it('is masked until the reader asks', () => {
|
|
34
|
+
/* Revealing is an action. It is never where the screen starts. */
|
|
35
|
+
expect(shownSecret('a-real-token', false)).toBe(MASK);
|
|
36
|
+
});
|
|
37
|
+
it('is the value once they have asked', () => {
|
|
38
|
+
expect(shownSecret('a-real-token', true)).toBe('a-real-token');
|
|
39
|
+
});
|
|
40
|
+
it('stays empty when there is nothing, asked or not', () => {
|
|
41
|
+
expect(shownSecret('', true)).toBe('');
|
|
42
|
+
expect(shownSecret(undefined, true)).toBe('');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('whether the buttons do anything', () => {
|
|
46
|
+
it('knows when there is something to copy', () => {
|
|
47
|
+
expect(hasSecret('token')).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('knows when there is not', () => {
|
|
50
|
+
/*
|
|
51
|
+
* A copy button over an absent token leaves the reader unable to tell a broken screen
|
|
52
|
+
* from an unissued credential.
|
|
53
|
+
*/
|
|
54
|
+
expect(hasSecret('')).toBe(false);
|
|
55
|
+
expect(hasSecret(null)).toBe(false);
|
|
56
|
+
expect(hasSecret(undefined)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=secret-display.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-display.test.js","sourceRoot":"","sources":["../../client/utils/secret-display.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE7E;;;;GAIG;AAEH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C;;;;WAIG;QACH,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,IAAI,GAAG,YAAY,CAAC,0EAA0E,CAAC,CAAA;QAErG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAClC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C;;;WAGG;QACH,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACnC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,kEAAkE;QAClE,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACtC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC;;;WAGG;QACH,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { hasSecret, MASK, maskedSecret, shownSecret } from './secret-display'\n\n/**\n * The app binding and appliance detail screens print a refresh token and an access token\n * into a readonly input, in plain text, from the moment the page opens. These are the rules\n * that replace that.\n */\n\ndescribe('the mask', () => {\n it('is the same width whatever it hides', () => {\n /*\n * ⚠ The rule worth testing, because one-dot-per-character is the obvious mask and it\n * leaks. A token's length says which kind of token it is; two masked fields of\n * different widths tell a reader which is the short one.\n */\n const short = maskedSecret('abc')\n const long = maskedSecret('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abcdefg')\n\n expect(short).toBe(long)\n expect(short.length).not.toBe(3)\n })\n\n it('says a secret is there', () => {\n expect(maskedSecret('anything')).toBe(MASK)\n })\n\n it('says nothing when there is no secret', () => {\n /*\n * Dots over an empty value send the reader hunting for a token nobody issued. The mask\n * is allowed to carry exactly one bit — present or absent — and this is it.\n */\n expect(maskedSecret('')).toBe('')\n expect(maskedSecret(null)).toBe('')\n expect(maskedSecret(undefined)).toBe('')\n })\n})\n\ndescribe('what the field shows', () => {\n it('is masked until the reader asks', () => {\n /* Revealing is an action. It is never where the screen starts. */\n expect(shownSecret('a-real-token', false)).toBe(MASK)\n })\n\n it('is the value once they have asked', () => {\n expect(shownSecret('a-real-token', true)).toBe('a-real-token')\n })\n\n it('stays empty when there is nothing, asked or not', () => {\n expect(shownSecret('', true)).toBe('')\n expect(shownSecret(undefined, true)).toBe('')\n })\n})\n\ndescribe('whether the buttons do anything', () => {\n it('knows when there is something to copy', () => {\n expect(hasSecret('token')).toBe(true)\n })\n\n it('knows when there is not', () => {\n /*\n * A copy button over an absent token leaves the reader unable to tell a broken screen\n * from an unissued credential.\n */\n expect(hasSecret('')).toBe(false)\n expect(hasSecret(null)).toBe(false)\n expect(hasSecret(undefined)).toBe(false)\n })\n})\n"]}
|