@1auth/authn 0.0.0-alpha.8 → 0.0.0-beta.0

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 (3) hide show
  1. package/index.js +424 -155
  2. package/package.json +53 -53
  3. package/LICENSE +0 -21
package/index.js CHANGED
@@ -1,163 +1,430 @@
1
- import { setTimeout } from 'node:timers/promises'
2
- import { randomId, makeSymetricKey } from '@1auth/crypto'
3
-
4
- export const options = {
5
- store: undefined,
6
- notify: undefined,
7
- table: 'credentials',
8
- authenticationDuration: 500, // min duration authentication should take (ms)
9
- usernameExists: [] // hooks to allow what to be used as a username
10
- }
11
- export default (params) => {
12
- Object.assign(options, params)
13
- }
14
- export const getOptions = () => options
15
-
16
- export const create = async (
17
- credentialType,
18
- { id, sub, value, ...rest },
19
- parentOptions
1
+ import { setTimeout } from "node:timers/promises";
2
+ import {
3
+ makeRandomConfigObject,
4
+ symmetricDecryptFields,
5
+ symmetricEncryptFields,
6
+ symmetricGenerateEncryptionKey,
7
+ } from "@1auth/crypto";
8
+
9
+ const id = "authn";
10
+
11
+ export const randomId = ({ prefix = "authn_", ...params } = {}) =>
12
+ makeRandomConfigObject({
13
+ id,
14
+ prefix,
15
+ ...params,
16
+ });
17
+
18
+ const defaults = {
19
+ id,
20
+ log: false,
21
+ store: undefined,
22
+ notify: undefined,
23
+ table: "authentications",
24
+ idGenerate: true,
25
+ randomId: randomId(),
26
+ authenticationDuration: 100, // minimum duration authentication should take (ms)
27
+ usernameExists: [], // hooks to allow what to be used as a username
28
+ encryptedFields: ["value"],
29
+ };
30
+ const options = {};
31
+ export default (opt = {}) => {
32
+ Object.assign(options, defaults, opt);
33
+ };
34
+ export const getOptions = () => options;
35
+
36
+ // export const exists = async (credentialOptions, sub, params) => {
37
+ // const type = makeType(credentialOptions);
38
+ // const list = await options.store.selectList(options.table, {
39
+ // ...params,
40
+ // sub,
41
+ // type,
42
+ // });
43
+ // return list.length > 1;
44
+ // };
45
+
46
+ export const count = async (credentialOptions, sub) => {
47
+ if (!sub || typeof sub !== "string") {
48
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
49
+ }
50
+ const type = makeType(credentialOptions);
51
+ const credentials = await options.store.selectList(
52
+ options.table,
53
+ { sub, type },
54
+ ["verify", "expire"],
55
+ );
56
+ let count = 0;
57
+ const now = nowInSeconds();
58
+ for (let i = credentials.length; i--; ) {
59
+ const credential = credentials[i];
60
+ if (credential.expire && credential.expire < now) {
61
+ continue;
62
+ }
63
+ if (!credential.verify) {
64
+ continue;
65
+ }
66
+ count += 1;
67
+ }
68
+ return count;
69
+ };
70
+
71
+ export const list = async (credentialOptions, sub, params, fields) => {
72
+ if (!sub || typeof sub !== "string") {
73
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
74
+ }
75
+ const type = makeType(credentialOptions);
76
+ const items = await options.store.selectList(
77
+ options.table,
78
+ {
79
+ ...params,
80
+ sub,
81
+ type,
82
+ },
83
+ fields,
84
+ );
85
+ // const now = nowInSeconds();
86
+ const list = [];
87
+ for (let i = items.length; i--; ) {
88
+ const item = items[i];
89
+ // TODO need filter for expire
90
+ // if (credential.expire < now) {
91
+ // continue;
92
+ // }
93
+ const { encryptionKey: encryptedKey } = item;
94
+ item.encryptionKey = undefined;
95
+ const decryptedItem = symmetricDecryptFields(
96
+ item,
97
+ { encryptedKey, sub },
98
+ options.encryptedFields,
99
+ );
100
+ list.push(decryptedItem);
101
+ }
102
+ return list;
103
+ };
104
+
105
+ const createCredential = async (
106
+ credentialOptions,
107
+ sub,
108
+ { id, value, ...values },
20
109
  ) => {
21
- const now = nowInSeconds()
22
- id ??= await randomId.create()
23
- const type = parentOptions.id + '-' + parentOptions[credentialType].type
24
- const otp = parentOptions[credentialType].otp
25
- const expire = parentOptions[credentialType].expire
26
- ? now + parentOptions[credentialType].expire
27
- : null
28
- const { encryptedKey } = makeSymetricKey(sub)
29
-
30
- const encryptedData = await parentOptions[credentialType].encode(
31
- value,
32
- encryptedKey,
33
- sub
34
- )
35
- await options.store.insert(options.table, {
36
- expire,
37
- ...rest,
38
- id,
39
- sub,
40
- type,
41
- otp,
42
- encryptionKey: encryptedKey,
43
- value: encryptedData,
44
- create: now,
45
- update: now
46
- })
47
- return id
48
- }
110
+ if (!sub || typeof sub !== "string") {
111
+ throw new Error("401 Unauthorized", { cause: { sub } });
112
+ }
113
+ const now = nowInSeconds();
114
+ const type = makeType(credentialOptions);
115
+ let { otp, expire } = credentialOptions;
116
+ expire &&= now + expire;
117
+
118
+ if (options.idGenerate) {
119
+ id ??= await options.randomId.create(options.idPrefix);
120
+ }
121
+ value ??= credentialOptions.create();
122
+ const encodedValue = await credentialOptions.encode(value);
49
123
 
124
+ const { encryptionKey, encryptedKey } = symmetricGenerateEncryptionKey(sub);
125
+ const encryptedValues = symmetricEncryptFields(
126
+ { ...values, value: encodedValue },
127
+ { encryptionKey, sub },
128
+ options.encryptedFields,
129
+ );
130
+ const params = {
131
+ ...encryptedValues,
132
+ sub,
133
+ type,
134
+ otp,
135
+ encryptionKey: encryptedKey,
136
+ create: now,
137
+ update: now,
138
+ };
139
+ if (expire) {
140
+ params.expire = expire;
141
+ }
142
+ if (options.idGenerate) {
143
+ params.id = id;
144
+ }
145
+ return params;
146
+ };
147
+
148
+ export const create = async (credentialOptions, sub, values) => {
149
+ const params = await createCredential(credentialOptions, sub, values);
150
+ const id = await options.store.insert(options.table, params);
151
+ return { ...params, id };
152
+ };
153
+
154
+ export const createList = async (credentialOptions, sub, list) => {
155
+ const rows = await Promise.all(
156
+ list.map((values) => createCredential(credentialOptions, sub, values)),
157
+ );
158
+ const res = await options.store.insertList(options.table, rows);
159
+ const params = rows[0];
160
+ return { ...params, id: res };
161
+ };
162
+
163
+ // Only used by webauthn to update counter within value
50
164
  export const update = async (
51
- credentialType,
52
- { id, sub, encryptionKey, value, ...rest },
53
- parentOptions
165
+ credentialOptions,
166
+ { id, sub, encryptionKey, encryptedKey, value, ...values },
54
167
  ) => {
55
- const now = nowInSeconds()
56
- const encryptedData = await parentOptions[credentialType].encode(
57
- value,
58
- encryptionKey,
59
- sub
60
- )
61
- return options.store.update(
62
- options.table,
63
- { id, sub },
64
- {
65
- ...rest,
66
- value: encryptedData,
67
- update: now
68
- }
69
- )
70
- }
168
+ // if (!sub || typeof sub !== "string") {
169
+ // throw new Error("401 Unauthorized", { cause: { sub } });
170
+ // }
171
+ const now = nowInSeconds();
172
+ // const type = makeType(credentialOptions);
173
+
174
+ const encodedValue = await credentialOptions.encode(value);
175
+
176
+ const encryptedValues = symmetricEncryptFields(
177
+ { ...values, value: encodedValue },
178
+ { encryptionKey, encryptedKey, sub },
179
+ options.encryptedFields,
180
+ );
181
+
182
+ return options.store.update(
183
+ options.table,
184
+ { sub, id },
185
+ {
186
+ ...encryptedValues,
187
+ update: now,
188
+ },
189
+ );
190
+ };
71
191
 
72
192
  export const subject = async (username) => {
73
- return Promise.all(
74
- options.usernameExists.map((exists) => exists(username))
75
- ).then((identities) => {
76
- return identities.filter((lookup) => lookup)?.[0]
77
- })
78
- }
79
-
80
- export const authenticate = async (username, secret, parentOptions) => {
81
- const timeout = setTimeout(() => {}, options.authenticationDuration)
82
- const type = parentOptions.id + '-' + parentOptions.secret.type
83
-
84
- const sub = await subject(username)
85
-
86
- const credentials = await options.store.selectList(options.table, {
87
- sub,
88
- type
89
- }) // TODO and verify is not null
90
- let valid, id, encryptionKey
91
- for (const credential of credentials) {
92
- let { value, encryptionKey: encryptedKey, ...rest } = credential
93
- value = await parentOptions.secret.decode(value, encryptedKey, sub)
94
- valid = await parentOptions.secret.verify(secret, value, rest)
95
- if (valid) {
96
- id ??= credential.id
97
- encryptionKey ??= encryptedKey
98
- break
99
- }
100
- }
101
-
102
- if (valid && parentOptions.secret.otp) {
103
- await options.store.remove(options.table, { id, sub })
104
- }
105
- await timeout
106
- if (!valid) throw new Error('401 Unauthorized')
107
- return { sub, id, encryptionKey, ...valid }
108
- }
109
-
110
- export const verifySecret = async (sub, id, parentOptions) => {
111
- // const type = parentOptions.id + '-' + parentOptions.secret.type
112
- await options.store.update(
113
- options.table,
114
- { id, sub },
115
- { verify: nowInSeconds() }
116
- )
117
- }
118
-
119
- export const verify = async (credentialType, sub, token, parentOptions) => {
120
- const timeout = setTimeout(() => {}, options.authenticationDuration)
121
- const type = parentOptions.id + '-' + parentOptions[credentialType].type
122
- let id
123
- const credentials = await options.store.selectList(options.table, {
124
- sub,
125
- type
126
- })
127
- // TODO re-confirm when needed
128
- // .then((rows) => {
129
- // if (rows.length) {
130
- // return rows
131
- // }
132
- //
133
- // return options.store.select(options.table, { id: sub, type })
134
- // })
135
-
136
- let valid
137
- for (const credential of credentials) {
138
- let { value, encryptionKey, ...rest } = credential
139
- value = await parentOptions[credentialType].decode(
140
- value,
141
- encryptionKey,
142
- sub
143
- )
144
- valid = await parentOptions[credentialType].verify(token, value, rest)
145
- if (valid) {
146
- id = credential.id
147
- break
148
- }
149
- }
150
- if (valid && parentOptions[credentialType].otp) {
151
- await options.store.remove(options.table, { id, sub })
152
- }
153
- if (!valid) throw new Error('401 Unauthorized')
154
- await timeout
155
- return { sub, id, ...valid }
156
- }
157
-
158
- export const expire = async (sub, id, parentOptions = options) => {
159
- await options.store.remove(options.table, { id, sub })
160
- }
193
+ return Promise.all(
194
+ options.usernameExists.map((exists) => {
195
+ return exists(username);
196
+ }),
197
+ ).then((identities) => {
198
+ return identities.filter((lookup) => lookup)?.[0];
199
+ });
200
+ };
201
+
202
+ export const authenticate = async (credentialOptions, username, secret) => {
203
+ const timeout = setTimeout(options.authenticationDuration);
204
+
205
+ const sub = await subject(username);
206
+ if (!sub) {
207
+ await timeout;
208
+ throw new Error("401 Unauthorized", { cause: { username } });
209
+ }
210
+ const type = makeType(credentialOptions);
211
+
212
+ const credentials = await options.store.selectList(
213
+ options.table,
214
+ {
215
+ sub,
216
+ type,
217
+ },
218
+ ["id", "encryptionKey", "value", "otp", "verify", "expire", "sourceId"],
219
+ );
220
+ const now = nowInSeconds();
221
+ let valid;
222
+ let skipIgnoredCount = 0;
223
+ let skipExpiredCount = 0;
224
+ for (const credential of credentials) {
225
+ // non-opt credentials must be verified before use
226
+ if (!credential.otp && !credential.verify) {
227
+ skipIgnoredCount += 1;
228
+ continue;
229
+ }
230
+ // skip expired
231
+
232
+ if (credential.expire && credential.expire < now) {
233
+ skipExpiredCount += 1;
234
+ continue;
235
+ }
236
+ const { encryptionKey: encryptedKey } = credential;
237
+ const decryptedCredential = symmetricDecryptFields(
238
+ credential,
239
+ { encryptedKey, sub },
240
+ options.encryptedFields,
241
+ );
242
+ let { value, ...values } = decryptedCredential;
243
+ value = await credentialOptions.decode(value);
244
+ try {
245
+ valid = await credentialOptions.verify(secret, value, values);
246
+ } catch (e) {
247
+ if (options.log) {
248
+ options.log(e);
249
+ }
250
+ continue;
251
+ }
252
+ if (valid) {
253
+ const { id, otp } = credential;
254
+ if (otp) {
255
+ await expire(credentialOptions, sub, id, { lastused: now });
256
+ } else {
257
+ const now = nowInSeconds();
258
+ await options.store.update(
259
+ options.table,
260
+ { id, sub },
261
+ { lastused: now },
262
+ );
263
+ }
264
+
265
+ if (credentialOptions.cleanup) {
266
+ await credentialOptions.cleanup(sub, value, values);
267
+ }
268
+ break;
269
+ }
270
+ }
271
+
272
+ await timeout;
273
+ if (!valid) {
274
+ let cause = "invalid";
275
+ const credentialsCount = credentials.length - skipIgnoredCount;
276
+ if (credentialsCount === 0) {
277
+ cause = "missing";
278
+ } else if (skipExpiredCount === credentialsCount) {
279
+ cause = "expired";
280
+ }
281
+ throw new Error("401 Unauthorized", { cause });
282
+ }
283
+ return sub;
284
+ };
285
+
286
+ export const verifySecret = async (_credentialOptions, sub, id) => {
287
+ if (!sub || typeof sub !== "string") {
288
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
289
+ }
290
+ if (!id || typeof id !== "string") {
291
+ throw new Error("404 Not Found", { cause: { sub, id } });
292
+ }
293
+ // const type = makeType(credentialOptions);
294
+ const now = nowInSeconds();
295
+ await options.store.update(
296
+ options.table,
297
+ { sub, id },
298
+ { update: now, verify: now },
299
+ );
300
+ };
301
+
302
+ // TODO add in sourceId as filter for messengers
303
+ export const verify = async (credentialOptions, sub, input) => {
304
+ const timeout = setTimeout(options.authenticationDuration);
305
+ if (!sub || typeof sub !== "string") {
306
+ await timeout;
307
+ throw new Error("401 Unauthorized", { cause: { sub } });
308
+ }
309
+ // Can be string or json (webauthn)
310
+ if (!input) {
311
+ await timeout;
312
+ throw new Error("401 Unauthorized", { cause: { sub, input } });
313
+ }
314
+
315
+ const type = makeType(credentialOptions);
316
+
317
+ const credentials = await options.store.selectList(options.table, {
318
+ sub,
319
+ type,
320
+ });
321
+
322
+ const now = nowInSeconds();
323
+ let valid;
324
+ let credential;
325
+ let skipExpiredCount = 0;
326
+ for (credential of credentials) {
327
+ // skip expired
328
+ if (credential.expire < now) {
329
+ skipExpiredCount += 1;
330
+ continue;
331
+ }
332
+ const { encryptionKey: encryptedKey } = credential;
333
+ const decryptedCredential = symmetricDecryptFields(
334
+ credential,
335
+ { encryptedKey, sub },
336
+ options.encryptedFields,
337
+ );
338
+ let { value, ...values } = decryptedCredential;
339
+ value = await credentialOptions.decode(value);
340
+ try {
341
+ valid = await credentialOptions.verify(input, value, values);
342
+ } catch (e) {
343
+ if (options.log) {
344
+ options.log(e);
345
+ }
346
+ continue;
347
+ }
348
+ if (valid) {
349
+ const { id, otp } = credential;
350
+ if (otp) {
351
+ await remove(credentialOptions, sub, id);
352
+ }
353
+ break;
354
+ }
355
+ }
356
+
357
+ await timeout;
358
+
359
+ if (!valid) {
360
+ let cause = "invalid";
361
+ const credentialsCount = credentials.length;
362
+ if (credentialsCount === 0) {
363
+ cause = "missing";
364
+ } else if (skipExpiredCount === credentialsCount) {
365
+ cause = "expired";
366
+ }
367
+ throw new Error("401 Unauthorized", { cause });
368
+ }
369
+ return { ...credential, ...valid };
370
+ };
371
+
372
+ export const expire = async (_credentialOptions, sub, id, values = {}) => {
373
+ if (!sub || typeof sub !== "string") {
374
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
375
+ }
376
+ if (!id || typeof id !== "string") {
377
+ throw new Error("404 Not Found", { cause: { sub, id } });
378
+ }
379
+ // const type = makeType(credentialOptions);
380
+ await options.store.update(
381
+ options.table,
382
+ { sub, id },
383
+ { ...values, expire: nowInSeconds() - 1 },
384
+ );
385
+ };
386
+
387
+ export const remove = async (credentialOptions, sub, id) => {
388
+ if (!sub || typeof sub !== "string") {
389
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
390
+ }
391
+ if (!id || typeof id !== "string") {
392
+ throw new Error("404 Not Found", { cause: { sub, id } });
393
+ }
394
+ const type = makeType(credentialOptions);
395
+ await options.store.remove(options.table, { id, type, sub });
396
+ };
397
+
398
+ export const removeList = async (credentialOptions, sub, id) => {
399
+ if (!sub || typeof sub !== "string") {
400
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
401
+ }
402
+ if (!id || !Array.isArray(id) || !id.length) {
403
+ throw new Error("404 Not Found", { cause: { sub, id } });
404
+ }
405
+ const type = makeType(credentialOptions);
406
+ await options.store.removeList(options.table, { id, type, sub });
407
+ };
408
+
409
+ export const select = async (credentialOptions, sub, id) => {
410
+ if (!sub || typeof sub !== "string") {
411
+ throw new Error("401 Unauthorized", { cause: { sub, id } });
412
+ }
413
+ if (!id || typeof id !== "string") {
414
+ throw new Error("404 Not Found", { cause: { sub, id } });
415
+ }
416
+ const type = makeType(credentialOptions);
417
+ const item = await options.store.select(options.table, { id, type, sub });
418
+ if (!item) return item;
419
+ const { encryptionKey: encryptedKey } = item;
420
+ item.encryptionKey = undefined;
421
+ const decryptedItem = symmetricDecryptFields(
422
+ item,
423
+ { encryptedKey, sub },
424
+ options.encryptedFields,
425
+ );
426
+ return decryptedItem;
427
+ };
161
428
 
162
429
  // TODO manage onboard state
163
430
 
@@ -165,4 +432,6 @@ export const expire = async (sub, id, parentOptions = options) => {
165
432
 
166
433
  // TODO authorize management?
167
434
 
168
- const nowInSeconds = () => Math.floor(Date.now() / 1000)
435
+ const makeType = (credentialOptions) =>
436
+ `${credentialOptions.id}-${credentialOptions.type}`;
437
+ const nowInSeconds = () => Math.floor(Date.now() / 1000);
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
1
  {
2
- "name": "@1auth/authn",
3
- "version": "0.0.0-alpha.8",
4
- "description": "",
5
- "type": "module",
6
- "engines": {
7
- "node": ">=16"
8
- },
9
- "engineStrict": true,
10
- "publishConfig": {
11
- "access": "public"
12
- },
13
- "main": "./index.js",
14
- "module": "./index.js",
15
- "exports": {
16
- ".": {
17
- "import": {
18
- "types": "./index.d.ts",
19
- "default": "./index.js"
20
- }
21
- }
22
- },
23
- "types": "index.d.ts",
24
- "files": [
25
- "index.js",
26
- "index.d.ts"
27
- ],
28
- "scripts": {
29
- "test": "npm run test:unit",
30
- "test:unit": "ava"
31
- },
32
- "license": "MIT",
33
- "funding": {
34
- "type": "github",
35
- "url": "https://github.com/sponsors/willfarrell"
36
- },
37
- "keywords": [],
38
- "author": {
39
- "name": "1auth contributors",
40
- "url": "https://github.com/willfarrell/1auth/graphs/contributors"
41
- },
42
- "repository": {
43
- "type": "git",
44
- "url": "github:willfarrell/1auth",
45
- "directory": "packages/authn"
46
- },
47
- "bugs": {
48
- "url": "https://github.com/willfarrell/1auth/issues"
49
- },
50
- "homepage": "https://github.com/willfarrell/1auth",
51
- "gitHead": "d21e1013f55ed05af4daf980bc4dfdfd52538792",
52
- "dependencies": {
53
- "@1auth/crypto": "0.0.0-alpha.8"
54
- }
2
+ "name": "@1auth/authn",
3
+ "version": "0.0.0-beta.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "engineStrict": true,
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "main": "./index.js",
14
+ "module": "./index.js",
15
+ "exports": {
16
+ ".": {
17
+ "import": {
18
+ "types": "./index.d.ts",
19
+ "default": "./index.js"
20
+ }
21
+ }
22
+ },
23
+ "types": "index.d.ts",
24
+ "files": [
25
+ "index.js",
26
+ "index.d.ts"
27
+ ],
28
+ "scripts": {
29
+ "test": "npm run test:unit",
30
+ "test:unit": "node --test"
31
+ },
32
+ "license": "MIT",
33
+ "funding": {
34
+ "type": "github",
35
+ "url": "https://github.com/sponsors/willfarrell"
36
+ },
37
+ "keywords": [],
38
+ "author": {
39
+ "name": "1auth contributors",
40
+ "url": "https://github.com/willfarrell/1auth/graphs/contributors"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/willfarrell/1auth.git",
45
+ "directory": "packages/authn"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/willfarrell/1auth/issues"
49
+ },
50
+ "homepage": "https://github.com/willfarrell/1auth",
51
+ "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
52
+ "dependencies": {
53
+ "@1auth/crypto": "0.0.0-beta.0"
54
+ }
55
55
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 will Farrell
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.