@atproto/identity 0.2.1 → 0.3.1
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/CHANGELOG.md +23 -0
- package/LICENSE.txt +7 -0
- package/README.md +6 -1
- package/dist/did/atproto-data.d.ts +3 -5
- package/dist/did/base-resolver.d.ts +2 -2
- package/dist/index.js +132 -111
- package/dist/index.js.map +3 -3
- package/dist/types.d.ts +6 -94
- package/package.json +3 -4
- package/src/did/atproto-data.ts +27 -89
- package/src/did/base-resolver.ts +22 -11
- package/src/did/memory-cache.ts +1 -2
- package/src/handle/index.ts +1 -1
- package/src/types.ts +11 -24
- package/LICENSE +0 -21
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DidDocument } from '@atproto/common-web';
|
|
2
|
+
export { didDocument } from '@atproto/common-web';
|
|
3
|
+
export type { DidDocument } from '@atproto/common-web';
|
|
2
4
|
export declare type IdentityResolverOpts = {
|
|
3
5
|
timeout?: number;
|
|
4
6
|
plcUrl?: string;
|
|
@@ -25,102 +27,12 @@ export declare type CacheResult = {
|
|
|
25
27
|
doc: DidDocument;
|
|
26
28
|
updatedAt: number;
|
|
27
29
|
stale: boolean;
|
|
30
|
+
expired: boolean;
|
|
28
31
|
};
|
|
29
32
|
export interface DidCache {
|
|
30
|
-
cacheDid(did: string, doc: DidDocument): Promise<void>;
|
|
33
|
+
cacheDid(did: string, doc: DidDocument, prevResult?: CacheResult): Promise<void>;
|
|
31
34
|
checkCache(did: string): Promise<CacheResult | null>;
|
|
32
|
-
refreshCache(did: string, getDoc: () => Promise<DidDocument | null
|
|
35
|
+
refreshCache(did: string, getDoc: () => Promise<DidDocument | null>, prevResult?: CacheResult): Promise<void>;
|
|
33
36
|
clearEntry(did: string): Promise<void>;
|
|
34
37
|
clear(): Promise<void>;
|
|
35
38
|
}
|
|
36
|
-
export declare const verificationMethod: z.ZodObject<{
|
|
37
|
-
id: z.ZodString;
|
|
38
|
-
type: z.ZodString;
|
|
39
|
-
controller: z.ZodString;
|
|
40
|
-
publicKeyMultibase: z.ZodOptional<z.ZodString>;
|
|
41
|
-
}, "strip", z.ZodTypeAny, {
|
|
42
|
-
id: string;
|
|
43
|
-
type: string;
|
|
44
|
-
controller: string;
|
|
45
|
-
publicKeyMultibase?: string | undefined;
|
|
46
|
-
}, {
|
|
47
|
-
id: string;
|
|
48
|
-
type: string;
|
|
49
|
-
controller: string;
|
|
50
|
-
publicKeyMultibase?: string | undefined;
|
|
51
|
-
}>;
|
|
52
|
-
export declare const service: z.ZodObject<{
|
|
53
|
-
id: z.ZodString;
|
|
54
|
-
type: z.ZodString;
|
|
55
|
-
serviceEndpoint: z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
56
|
-
}, "strip", z.ZodTypeAny, {
|
|
57
|
-
id: string;
|
|
58
|
-
type: string;
|
|
59
|
-
serviceEndpoint: (string | Record<string, unknown>) & (string | Record<string, unknown> | undefined);
|
|
60
|
-
}, {
|
|
61
|
-
id: string;
|
|
62
|
-
type: string;
|
|
63
|
-
serviceEndpoint: (string | Record<string, unknown>) & (string | Record<string, unknown> | undefined);
|
|
64
|
-
}>;
|
|
65
|
-
export declare const didDocument: z.ZodObject<{
|
|
66
|
-
id: z.ZodString;
|
|
67
|
-
alsoKnownAs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
68
|
-
verificationMethod: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
69
|
-
id: z.ZodString;
|
|
70
|
-
type: z.ZodString;
|
|
71
|
-
controller: z.ZodString;
|
|
72
|
-
publicKeyMultibase: z.ZodOptional<z.ZodString>;
|
|
73
|
-
}, "strip", z.ZodTypeAny, {
|
|
74
|
-
id: string;
|
|
75
|
-
type: string;
|
|
76
|
-
controller: string;
|
|
77
|
-
publicKeyMultibase?: string | undefined;
|
|
78
|
-
}, {
|
|
79
|
-
id: string;
|
|
80
|
-
type: string;
|
|
81
|
-
controller: string;
|
|
82
|
-
publicKeyMultibase?: string | undefined;
|
|
83
|
-
}>, "many">>;
|
|
84
|
-
service: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
85
|
-
id: z.ZodString;
|
|
86
|
-
type: z.ZodString;
|
|
87
|
-
serviceEndpoint: z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
88
|
-
}, "strip", z.ZodTypeAny, {
|
|
89
|
-
id: string;
|
|
90
|
-
type: string;
|
|
91
|
-
serviceEndpoint: (string | Record<string, unknown>) & (string | Record<string, unknown> | undefined);
|
|
92
|
-
}, {
|
|
93
|
-
id: string;
|
|
94
|
-
type: string;
|
|
95
|
-
serviceEndpoint: (string | Record<string, unknown>) & (string | Record<string, unknown> | undefined);
|
|
96
|
-
}>, "many">>;
|
|
97
|
-
}, "strip", z.ZodTypeAny, {
|
|
98
|
-
id: string;
|
|
99
|
-
alsoKnownAs?: string[] | undefined;
|
|
100
|
-
verificationMethod?: {
|
|
101
|
-
id: string;
|
|
102
|
-
type: string;
|
|
103
|
-
controller: string;
|
|
104
|
-
publicKeyMultibase?: string | undefined;
|
|
105
|
-
}[] | undefined;
|
|
106
|
-
service?: {
|
|
107
|
-
id: string;
|
|
108
|
-
type: string;
|
|
109
|
-
serviceEndpoint: (string | Record<string, unknown>) & (string | Record<string, unknown> | undefined);
|
|
110
|
-
}[] | undefined;
|
|
111
|
-
}, {
|
|
112
|
-
id: string;
|
|
113
|
-
alsoKnownAs?: string[] | undefined;
|
|
114
|
-
verificationMethod?: {
|
|
115
|
-
id: string;
|
|
116
|
-
type: string;
|
|
117
|
-
controller: string;
|
|
118
|
-
publicKeyMultibase?: string | undefined;
|
|
119
|
-
}[] | undefined;
|
|
120
|
-
service?: {
|
|
121
|
-
id: string;
|
|
122
|
-
type: string;
|
|
123
|
-
serviceEndpoint: (string | Record<string, unknown>) & (string | Record<string, unknown> | undefined);
|
|
124
|
-
}[] | undefined;
|
|
125
|
-
}>;
|
|
126
|
-
export declare type DidDocument = z.infer<typeof didDocument>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Library for decentralized identities in atproto using DIDs and handles",
|
|
6
6
|
"keywords": [
|
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
"main": "dist/index.js",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"axios": "^0.27.2",
|
|
20
|
-
"
|
|
21
|
-
"@atproto/
|
|
22
|
-
"@atproto/crypto": "^0.2.2"
|
|
20
|
+
"@atproto/common-web": "^0.2.3",
|
|
21
|
+
"@atproto/crypto": "^0.2.3"
|
|
23
22
|
},
|
|
24
23
|
"devDependencies": {
|
|
25
24
|
"@did-plc/lib": "^0.0.1",
|
package/src/did/atproto-data.ts
CHANGED
|
@@ -1,73 +1,39 @@
|
|
|
1
1
|
import * as crypto from '@atproto/crypto'
|
|
2
2
|
import { DidDocument, AtprotoData } from '../types'
|
|
3
|
+
import {
|
|
4
|
+
getDid,
|
|
5
|
+
getHandle,
|
|
6
|
+
getPdsEndpoint,
|
|
7
|
+
getFeedGenEndpoint,
|
|
8
|
+
getNotifEndpoint,
|
|
9
|
+
getSigningKey,
|
|
10
|
+
} from '@atproto/common-web'
|
|
3
11
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
export {
|
|
13
|
+
getDid,
|
|
14
|
+
getHandle,
|
|
15
|
+
getPdsEndpoint as getPds,
|
|
16
|
+
getFeedGenEndpoint as getFeedGen,
|
|
17
|
+
getNotifEndpoint as getNotif,
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export const getKey = (doc: DidDocument): string | undefined => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
if (!keys) return undefined
|
|
16
|
-
if (typeof keys !== 'object') return undefined
|
|
17
|
-
if (!Array.isArray(keys)) {
|
|
18
|
-
keys = [keys]
|
|
19
|
-
}
|
|
20
|
-
const found = keys.find(
|
|
21
|
-
(key) => key.id === '#atproto' || key.id === `${did}#atproto`,
|
|
22
|
-
)
|
|
23
|
-
if (!found) return undefined
|
|
21
|
+
const key = getSigningKey(doc)
|
|
22
|
+
if (!key) return undefined
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
// should we be surfacing errors here or returning undefined?
|
|
27
|
-
if (!found.publicKeyMultibase) return undefined
|
|
28
|
-
const keyBytes = crypto.multibaseToBytes(found.publicKeyMultibase)
|
|
24
|
+
const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)
|
|
29
25
|
let didKey: string | undefined = undefined
|
|
30
|
-
if (
|
|
26
|
+
if (key.type === 'EcdsaSecp256r1VerificationKey2019') {
|
|
31
27
|
didKey = crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)
|
|
32
|
-
} else if (
|
|
28
|
+
} else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {
|
|
33
29
|
didKey = crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)
|
|
34
|
-
} else if (
|
|
35
|
-
const parsed = crypto.parseMultikey(
|
|
30
|
+
} else if (key.type === 'Multikey') {
|
|
31
|
+
const parsed = crypto.parseMultikey(key.publicKeyMultibase)
|
|
36
32
|
didKey = crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)
|
|
37
33
|
}
|
|
38
34
|
return didKey
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
export const getHandle = (doc: DidDocument): string | undefined => {
|
|
42
|
-
const aka = doc.alsoKnownAs
|
|
43
|
-
if (!aka) return undefined
|
|
44
|
-
const found = aka.find((name) => name.startsWith('at://'))
|
|
45
|
-
if (!found) return undefined
|
|
46
|
-
// strip off at:// prefix
|
|
47
|
-
return found.slice(5)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const getPds = (doc: DidDocument): string | undefined => {
|
|
51
|
-
return getServiceEndpoint(doc, {
|
|
52
|
-
id: '#atproto_pds',
|
|
53
|
-
type: 'AtprotoPersonalDataServer',
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const getFeedGen = (doc: DidDocument): string | undefined => {
|
|
58
|
-
return getServiceEndpoint(doc, {
|
|
59
|
-
id: '#bsky_fg',
|
|
60
|
-
type: 'BskyFeedGenerator',
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const getNotif = (doc: DidDocument): string | undefined => {
|
|
65
|
-
return getServiceEndpoint(doc, {
|
|
66
|
-
id: '#bsky_notif',
|
|
67
|
-
type: 'BskyNotificationService',
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
37
|
export const parseToAtprotoDocument = (
|
|
72
38
|
doc: DidDocument,
|
|
73
39
|
): Partial<AtprotoData> => {
|
|
@@ -76,7 +42,7 @@ export const parseToAtprotoDocument = (
|
|
|
76
42
|
did,
|
|
77
43
|
signingKey: getKey(doc),
|
|
78
44
|
handle: getHandle(doc),
|
|
79
|
-
pds:
|
|
45
|
+
pds: getPdsEndpoint(doc),
|
|
80
46
|
}
|
|
81
47
|
}
|
|
82
48
|
|
|
@@ -97,38 +63,10 @@ export const ensureAtpDocument = (doc: DidDocument): AtprotoData => {
|
|
|
97
63
|
return { did, signingKey, handle, pds }
|
|
98
64
|
}
|
|
99
65
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new Error('Invalid pds protocol')
|
|
105
|
-
}
|
|
106
|
-
if (!hostname) {
|
|
107
|
-
throw new Error('Invalid pds hostname')
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const getServiceEndpoint = (
|
|
112
|
-
doc: DidDocument,
|
|
113
|
-
opts: { id: string; type: string },
|
|
114
|
-
) => {
|
|
115
|
-
const did = getDid(doc)
|
|
116
|
-
let services = doc.service
|
|
117
|
-
if (!services) return undefined
|
|
118
|
-
if (typeof services !== 'object') return undefined
|
|
119
|
-
if (!Array.isArray(services)) {
|
|
120
|
-
services = [services]
|
|
121
|
-
}
|
|
122
|
-
const found = services.find(
|
|
123
|
-
(service) => service.id === opts.id || service.id === `${did}${opts.id}`,
|
|
124
|
-
)
|
|
125
|
-
if (!found) return undefined
|
|
126
|
-
if (found.type !== opts.type) {
|
|
127
|
-
return undefined
|
|
128
|
-
}
|
|
129
|
-
if (typeof found.serviceEndpoint !== 'string') {
|
|
130
|
-
return undefined
|
|
66
|
+
export const ensureAtprotoKey = (doc: DidDocument): string => {
|
|
67
|
+
const { signingKey } = parseToAtprotoDocument(doc)
|
|
68
|
+
if (!signingKey) {
|
|
69
|
+
throw new Error(`Could not parse signingKey from doc: ${doc}`)
|
|
131
70
|
}
|
|
132
|
-
|
|
133
|
-
return found.serviceEndpoint
|
|
71
|
+
return signingKey
|
|
134
72
|
}
|
package/src/did/base-resolver.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import * as crypto from '@atproto/crypto'
|
|
2
2
|
import { check } from '@atproto/common-web'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DidCache,
|
|
5
|
+
AtprotoData,
|
|
6
|
+
DidDocument,
|
|
7
|
+
didDocument,
|
|
8
|
+
CacheResult,
|
|
9
|
+
} from '../types'
|
|
4
10
|
import * as atprotoData from './atproto-data'
|
|
5
11
|
import { DidNotFoundError, PoorlyFormattedDidDocumentError } from '../errors'
|
|
6
12
|
|
|
@@ -25,20 +31,25 @@ export abstract class BaseResolver {
|
|
|
25
31
|
return this.validateDidDoc(did, got)
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
async refreshCache(did: string): Promise<void> {
|
|
29
|
-
await this.cache?.refreshCache(
|
|
34
|
+
async refreshCache(did: string, prevResult?: CacheResult): Promise<void> {
|
|
35
|
+
await this.cache?.refreshCache(
|
|
36
|
+
did,
|
|
37
|
+
() => this.resolveNoCache(did),
|
|
38
|
+
prevResult,
|
|
39
|
+
)
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
async resolve(
|
|
33
43
|
did: string,
|
|
34
44
|
forceRefresh = false,
|
|
35
45
|
): Promise<DidDocument | null> {
|
|
46
|
+
let fromCache: CacheResult | null = null
|
|
36
47
|
if (this.cache && !forceRefresh) {
|
|
37
|
-
|
|
38
|
-
if (fromCache
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
fromCache = await this.cache.checkCache(did)
|
|
49
|
+
if (fromCache && !fromCache.expired) {
|
|
50
|
+
if (fromCache?.stale) {
|
|
51
|
+
await this.refreshCache(did, fromCache)
|
|
52
|
+
}
|
|
42
53
|
return fromCache.doc
|
|
43
54
|
}
|
|
44
55
|
}
|
|
@@ -48,7 +59,7 @@ export abstract class BaseResolver {
|
|
|
48
59
|
await this.cache?.clearEntry(did)
|
|
49
60
|
return null
|
|
50
61
|
}
|
|
51
|
-
await this.cache?.cacheDid(did, got)
|
|
62
|
+
await this.cache?.cacheDid(did, got, fromCache ?? undefined)
|
|
52
63
|
return got
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -72,8 +83,8 @@ export abstract class BaseResolver {
|
|
|
72
83
|
if (did.startsWith('did:key:')) {
|
|
73
84
|
return did
|
|
74
85
|
} else {
|
|
75
|
-
const
|
|
76
|
-
return
|
|
86
|
+
const didDocument = await this.ensureResolve(did, forceRefresh)
|
|
87
|
+
return atprotoData.ensureAtprotoKey(didDocument)
|
|
77
88
|
}
|
|
78
89
|
}
|
|
79
90
|
|
package/src/did/memory-cache.ts
CHANGED
|
@@ -35,13 +35,12 @@ export class MemoryCache implements DidCache {
|
|
|
35
35
|
if (!val) return null
|
|
36
36
|
const now = Date.now()
|
|
37
37
|
const expired = now > val.updatedAt + this.maxTTL
|
|
38
|
-
if (expired) return null
|
|
39
|
-
|
|
40
38
|
const stale = now > val.updatedAt + this.staleTTL
|
|
41
39
|
return {
|
|
42
40
|
...val,
|
|
43
41
|
did,
|
|
44
42
|
stale,
|
|
43
|
+
expired,
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
|
package/src/handle/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ export class HandleResolver {
|
|
|
50
50
|
const url = new URL('/.well-known/atproto-did', `https://${handle}`)
|
|
51
51
|
try {
|
|
52
52
|
const res = await fetch(url, { signal })
|
|
53
|
-
const did = await res.text()
|
|
53
|
+
const did = (await res.text()).split('\n')[0].trim()
|
|
54
54
|
if (typeof did === 'string' && did.startsWith('did:')) {
|
|
55
55
|
return did
|
|
56
56
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DidDocument } from '@atproto/common-web'
|
|
2
|
+
|
|
3
|
+
export { didDocument } from '@atproto/common-web'
|
|
4
|
+
export type { DidDocument } from '@atproto/common-web'
|
|
2
5
|
|
|
3
6
|
export type IdentityResolverOpts = {
|
|
4
7
|
timeout?: number
|
|
@@ -30,37 +33,21 @@ export type CacheResult = {
|
|
|
30
33
|
doc: DidDocument
|
|
31
34
|
updatedAt: number
|
|
32
35
|
stale: boolean
|
|
36
|
+
expired: boolean
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export interface DidCache {
|
|
36
|
-
cacheDid(
|
|
40
|
+
cacheDid(
|
|
41
|
+
did: string,
|
|
42
|
+
doc: DidDocument,
|
|
43
|
+
prevResult?: CacheResult,
|
|
44
|
+
): Promise<void>
|
|
37
45
|
checkCache(did: string): Promise<CacheResult | null>
|
|
38
46
|
refreshCache(
|
|
39
47
|
did: string,
|
|
40
48
|
getDoc: () => Promise<DidDocument | null>,
|
|
49
|
+
prevResult?: CacheResult,
|
|
41
50
|
): Promise<void>
|
|
42
51
|
clearEntry(did: string): Promise<void>
|
|
43
52
|
clear(): Promise<void>
|
|
44
53
|
}
|
|
45
|
-
|
|
46
|
-
export const verificationMethod = z.object({
|
|
47
|
-
id: z.string(),
|
|
48
|
-
type: z.string(),
|
|
49
|
-
controller: z.string(),
|
|
50
|
-
publicKeyMultibase: z.string().optional(),
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
export const service = z.object({
|
|
54
|
-
id: z.string(),
|
|
55
|
-
type: z.string(),
|
|
56
|
-
serviceEndpoint: z.union([z.string(), z.record(z.unknown())]),
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
export const didDocument = z.object({
|
|
60
|
-
id: z.string(),
|
|
61
|
-
alsoKnownAs: z.array(z.string()).optional(),
|
|
62
|
-
verificationMethod: z.array(verificationMethod).optional(),
|
|
63
|
-
service: z.array(service).optional(),
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
export type DidDocument = z.infer<typeof didDocument>
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2022-2023 Bluesky PBC
|
|
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.
|