ox 0.12.2 → 0.12.3
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 +12 -0
- package/CoseKey/package.json +6 -0
- package/_cjs/core/Cbor.js +58 -0
- package/_cjs/core/Cbor.js.map +1 -1
- package/_cjs/core/CoseKey.js +41 -0
- package/_cjs/core/CoseKey.js.map +1 -0
- package/_cjs/core/WebAuthnP256.js +24 -5
- package/_cjs/core/WebAuthnP256.js.map +1 -1
- package/_cjs/index.js +3 -2
- package/_cjs/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/core/Cbor.js +59 -0
- package/_esm/core/Cbor.js.map +1 -1
- package/_esm/core/CoseKey.js +74 -0
- package/_esm/core/CoseKey.js.map +1 -0
- package/_esm/core/WebAuthnP256.js +75 -5
- package/_esm/core/WebAuthnP256.js.map +1 -1
- package/_esm/index.js +32 -0
- package/_esm/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/core/Cbor.d.ts.map +1 -1
- package/_types/core/CoseKey.d.ts +56 -0
- package/_types/core/CoseKey.d.ts.map +1 -0
- package/_types/core/WebAuthnP256.d.ts +71 -0
- package/_types/core/WebAuthnP256.d.ts.map +1 -1
- package/_types/index.d.ts +32 -0
- package/_types/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/core/Cbor.ts +64 -0
- package/core/CoseKey.ts +93 -0
- package/core/WebAuthnP256.ts +113 -4
- package/index.ts +32 -1
- package/package.json +6 -1
- package/version.ts +1 -1
package/core/Cbor.ts
CHANGED
|
@@ -254,6 +254,9 @@ function getEncodable(value: unknown): Encodable {
|
|
|
254
254
|
new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
|
|
255
255
|
)
|
|
256
256
|
|
|
257
|
+
if (value instanceof Map)
|
|
258
|
+
return getEncodable.map(value as Map<unknown, unknown>)
|
|
259
|
+
|
|
257
260
|
if (typeof value === 'object')
|
|
258
261
|
return getEncodable.object(value as Record<string, unknown>)
|
|
259
262
|
|
|
@@ -549,6 +552,67 @@ namespace getEncodable {
|
|
|
549
552
|
}
|
|
550
553
|
throw new ObjectTooLargeError({ size })
|
|
551
554
|
}
|
|
555
|
+
|
|
556
|
+
/** @internal */
|
|
557
|
+
export function map(value: Map<unknown, unknown>): Encodable {
|
|
558
|
+
const entries: { key: Encodable; value: Encodable }[] = []
|
|
559
|
+
for (const [k, v] of value)
|
|
560
|
+
entries.push({ key: getEncodable(k), value: getEncodable(v) })
|
|
561
|
+
const bodyLength = entries.reduce(
|
|
562
|
+
(acc, entry) => acc + entry.key.length + entry.value.length,
|
|
563
|
+
0,
|
|
564
|
+
)
|
|
565
|
+
const size = value.size
|
|
566
|
+
|
|
567
|
+
if (size <= 0x17)
|
|
568
|
+
return {
|
|
569
|
+
length: 1 + bodyLength,
|
|
570
|
+
encode(cursor) {
|
|
571
|
+
cursor.pushUint8(0xa0 + size)
|
|
572
|
+
for (const entry of entries) {
|
|
573
|
+
entry.key.encode(cursor)
|
|
574
|
+
entry.value.encode(cursor)
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
}
|
|
578
|
+
if (size <= 0xff)
|
|
579
|
+
return {
|
|
580
|
+
length: 2 + bodyLength,
|
|
581
|
+
encode(cursor) {
|
|
582
|
+
cursor.pushUint8(0xb8)
|
|
583
|
+
cursor.pushUint8(size)
|
|
584
|
+
for (const entry of entries) {
|
|
585
|
+
entry.key.encode(cursor)
|
|
586
|
+
entry.value.encode(cursor)
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
if (size <= 0xffff)
|
|
591
|
+
return {
|
|
592
|
+
length: 3 + bodyLength,
|
|
593
|
+
encode(cursor) {
|
|
594
|
+
cursor.pushUint8(0xb9)
|
|
595
|
+
cursor.pushUint16(size)
|
|
596
|
+
for (const entry of entries) {
|
|
597
|
+
entry.key.encode(cursor)
|
|
598
|
+
entry.value.encode(cursor)
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
}
|
|
602
|
+
if (size <= 0xffffffff)
|
|
603
|
+
return {
|
|
604
|
+
length: 5 + bodyLength,
|
|
605
|
+
encode(cursor) {
|
|
606
|
+
cursor.pushUint8(0xba)
|
|
607
|
+
cursor.pushUint32(size)
|
|
608
|
+
for (const entry of entries) {
|
|
609
|
+
entry.key.encode(cursor)
|
|
610
|
+
entry.value.encode(cursor)
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
throw new ObjectTooLargeError({ size })
|
|
615
|
+
}
|
|
552
616
|
}
|
|
553
617
|
|
|
554
618
|
/** @internal */
|
package/core/CoseKey.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as Cbor from './Cbor.js'
|
|
2
|
+
import * as Errors from './Errors.js'
|
|
3
|
+
import type * as Hex from './Hex.js'
|
|
4
|
+
import * as PublicKey from './PublicKey.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts a P256 {@link ox#PublicKey.PublicKey} to a CBOR-encoded COSE_Key.
|
|
8
|
+
*
|
|
9
|
+
* The COSE_Key uses integer map keys per [RFC 9053](https://datatracker.ietf.org/doc/html/rfc9053):
|
|
10
|
+
* - `1` (kty): `2` (EC2)
|
|
11
|
+
* - `3` (alg): `-7` (ES256)
|
|
12
|
+
* - `-1` (crv): `1` (P-256)
|
|
13
|
+
* - `-2` (x): x coordinate bytes
|
|
14
|
+
* - `-3` (y): y coordinate bytes
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts twoslash
|
|
18
|
+
* import { CoseKey, P256 } from 'ox'
|
|
19
|
+
*
|
|
20
|
+
* const { publicKey } = P256.createKeyPair()
|
|
21
|
+
*
|
|
22
|
+
* const coseKey = CoseKey.fromPublicKey(publicKey)
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param publicKey - The P256 public key to convert.
|
|
26
|
+
* @returns The CBOR-encoded COSE_Key as a Hex string.
|
|
27
|
+
*/
|
|
28
|
+
export function fromPublicKey(publicKey: PublicKey.PublicKey): Hex.Hex {
|
|
29
|
+
const pkBytes = PublicKey.toBytes(publicKey)
|
|
30
|
+
const x = pkBytes.slice(1, 33)
|
|
31
|
+
const y = pkBytes.slice(33, 65)
|
|
32
|
+
return Cbor.encode(
|
|
33
|
+
new Map<number, unknown>([
|
|
34
|
+
[1, 2], // kty: EC2
|
|
35
|
+
[3, -7], // alg: ES256
|
|
36
|
+
[-1, 1], // crv: P-256
|
|
37
|
+
[-2, x], // x coordinate
|
|
38
|
+
[-3, y], // y coordinate
|
|
39
|
+
]),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export declare namespace fromPublicKey {
|
|
44
|
+
type ErrorType =
|
|
45
|
+
| PublicKey.toBytes.ErrorType
|
|
46
|
+
| Cbor.encode.ErrorType
|
|
47
|
+
| Errors.GlobalErrorType
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Converts a CBOR-encoded COSE_Key to a P256 {@link ox#PublicKey.PublicKey}.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts twoslash
|
|
55
|
+
* import { CoseKey, P256 } from 'ox'
|
|
56
|
+
*
|
|
57
|
+
* const { publicKey } = P256.createKeyPair()
|
|
58
|
+
* const coseKey = CoseKey.fromPublicKey(publicKey)
|
|
59
|
+
*
|
|
60
|
+
* const publicKey2 = CoseKey.toPublicKey(coseKey)
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @param coseKey - The CBOR-encoded COSE_Key.
|
|
64
|
+
* @returns The P256 public key.
|
|
65
|
+
*/
|
|
66
|
+
export function toPublicKey(coseKey: Hex.Hex): PublicKey.PublicKey {
|
|
67
|
+
const decoded = Cbor.decode<Record<string, unknown>>(coseKey)
|
|
68
|
+
|
|
69
|
+
const x = decoded['-2']
|
|
70
|
+
const y = decoded['-3']
|
|
71
|
+
|
|
72
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array))
|
|
73
|
+
throw new InvalidCoseKeyError()
|
|
74
|
+
|
|
75
|
+
return PublicKey.from(new Uint8Array([0x04, ...x, ...y]))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export declare namespace toPublicKey {
|
|
79
|
+
type ErrorType =
|
|
80
|
+
| Cbor.decode.ErrorType
|
|
81
|
+
| PublicKey.from.ErrorType
|
|
82
|
+
| InvalidCoseKeyError
|
|
83
|
+
| Errors.GlobalErrorType
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Thrown when a COSE_Key does not contain valid P256 public key coordinates. */
|
|
87
|
+
export class InvalidCoseKeyError extends Errors.BaseError {
|
|
88
|
+
override readonly name = 'CoseKey.InvalidCoseKeyError'
|
|
89
|
+
|
|
90
|
+
constructor() {
|
|
91
|
+
super('COSE_Key does not contain valid P256 public key coordinates.')
|
|
92
|
+
}
|
|
93
|
+
}
|
package/core/WebAuthnP256.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Base64 from './Base64.js'
|
|
2
2
|
import * as Bytes from './Bytes.js'
|
|
3
|
+
import * as Cbor from './Cbor.js'
|
|
4
|
+
import * as CoseKey from './CoseKey.js'
|
|
3
5
|
import * as Errors from './Errors.js'
|
|
4
6
|
import * as Hash from './Hash.js'
|
|
5
7
|
import * as Hex from './Hex.js'
|
|
@@ -128,21 +130,69 @@ export declare namespace createCredential {
|
|
|
128
130
|
* // @log: "0xa379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce194705000001a4"
|
|
129
131
|
* ```
|
|
130
132
|
*
|
|
133
|
+
* @example
|
|
134
|
+
* ### With Attested Credential Data
|
|
135
|
+
*
|
|
136
|
+
* Include a credential ID and public key in the authenticator data (for registration responses):
|
|
137
|
+
*
|
|
138
|
+
* ```ts twoslash
|
|
139
|
+
* import { P256, WebAuthnP256 } from 'ox'
|
|
140
|
+
*
|
|
141
|
+
* const { publicKey } = P256.createKeyPair()
|
|
142
|
+
*
|
|
143
|
+
* const authenticatorData = WebAuthnP256.getAuthenticatorData({
|
|
144
|
+
* rpId: 'example.com',
|
|
145
|
+
* flag: 0x41, // UP + AT
|
|
146
|
+
* credential: {
|
|
147
|
+
* id: new Uint8Array(32),
|
|
148
|
+
* publicKey,
|
|
149
|
+
* },
|
|
150
|
+
* })
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
131
153
|
* @param options - Options to construct the authenticator data.
|
|
132
154
|
* @returns The authenticator data.
|
|
133
155
|
*/
|
|
134
156
|
export function getAuthenticatorData(
|
|
135
157
|
options: getAuthenticatorData.Options = {},
|
|
136
158
|
): Hex.Hex {
|
|
137
|
-
const {
|
|
159
|
+
const {
|
|
160
|
+
credential,
|
|
161
|
+
flag = 5,
|
|
162
|
+
rpId = window.location.hostname,
|
|
163
|
+
signCount = 0,
|
|
164
|
+
} = options
|
|
138
165
|
const rpIdHash = Hash.sha256(Hex.fromString(rpId))
|
|
139
166
|
const flag_bytes = Hex.fromNumber(flag, { size: 1 })
|
|
140
167
|
const signCount_bytes = Hex.fromNumber(signCount, { size: 4 })
|
|
141
|
-
|
|
168
|
+
const base = Hex.concat(rpIdHash, flag_bytes, signCount_bytes)
|
|
169
|
+
|
|
170
|
+
if (!credential) return base
|
|
171
|
+
|
|
172
|
+
// AAGUID (16 bytes of zeros)
|
|
173
|
+
const aaguid = Hex.fromBytes(new Uint8Array(16))
|
|
174
|
+
|
|
175
|
+
// Credential ID
|
|
176
|
+
const credentialId = Hex.fromBytes(credential.id)
|
|
177
|
+
const credIdLen = Hex.fromNumber(credential.id.length, { size: 2 })
|
|
178
|
+
|
|
179
|
+
// COSE public key
|
|
180
|
+
const coseKey = CoseKey.fromPublicKey(credential.publicKey)
|
|
181
|
+
|
|
182
|
+
return Hex.concat(base, aaguid, credIdLen, credentialId, coseKey)
|
|
142
183
|
}
|
|
143
184
|
|
|
144
185
|
export declare namespace getAuthenticatorData {
|
|
145
186
|
type Options = {
|
|
187
|
+
/** Attested credential data to include (credential ID + public key). When set, the AT flag (0x40) should also be set. */
|
|
188
|
+
credential?:
|
|
189
|
+
| {
|
|
190
|
+
/** The credential ID as raw bytes. */
|
|
191
|
+
id: Uint8Array
|
|
192
|
+
/** The P256 public key associated with the credential. */
|
|
193
|
+
publicKey: PublicKey.PublicKey
|
|
194
|
+
}
|
|
195
|
+
| undefined
|
|
146
196
|
/** A bitfield that indicates various attributes that were asserted by the authenticator. [Read more](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data#flags) */
|
|
147
197
|
flag?: number | undefined
|
|
148
198
|
/** The [Relying Party ID](https://w3c.github.io/webauthn/#relying-party-identifier) that the credential is scoped to. */
|
|
@@ -187,10 +237,11 @@ export function getClientDataJSON(options: getClientDataJSON.Options): string {
|
|
|
187
237
|
crossOrigin = false,
|
|
188
238
|
extraClientData,
|
|
189
239
|
origin = window.location.origin,
|
|
240
|
+
type = 'webauthn.get',
|
|
190
241
|
} = options
|
|
191
242
|
|
|
192
243
|
return JSON.stringify({
|
|
193
|
-
type
|
|
244
|
+
type,
|
|
194
245
|
challenge: Base64.fromHex(challenge, { url: true, pad: false }),
|
|
195
246
|
origin,
|
|
196
247
|
crossOrigin,
|
|
@@ -208,11 +259,66 @@ export declare namespace getClientDataJSON {
|
|
|
208
259
|
extraClientData?: Record<string, unknown> | undefined
|
|
209
260
|
/** The fully qualified origin of the relying party which has been given by the client/browser to the authenticator. */
|
|
210
261
|
origin?: string | undefined
|
|
262
|
+
/** The WebAuthn ceremony type. @default 'webauthn.get' */
|
|
263
|
+
type?: 'webauthn.create' | 'webauthn.get' | undefined
|
|
211
264
|
}
|
|
212
265
|
|
|
213
266
|
type ErrorType = Errors.GlobalErrorType
|
|
214
267
|
}
|
|
215
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Constructs a CBOR-encoded attestation object for testing WebAuthn registration
|
|
271
|
+
* verification. Combines the authenticator data with an attestation statement.
|
|
272
|
+
*
|
|
273
|
+
* :::warning
|
|
274
|
+
*
|
|
275
|
+
* This function is mainly for testing purposes. In production, the attestation
|
|
276
|
+
* object is returned by the authenticator during `navigator.credentials.create()`.
|
|
277
|
+
*
|
|
278
|
+
* :::
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts twoslash
|
|
282
|
+
* import { P256, WebAuthnP256 } from 'ox'
|
|
283
|
+
*
|
|
284
|
+
* const { publicKey } = P256.createKeyPair()
|
|
285
|
+
*
|
|
286
|
+
* const attestationObject = WebAuthnP256.getAttestationObject({
|
|
287
|
+
* authData: WebAuthnP256.getAuthenticatorData({
|
|
288
|
+
* rpId: 'example.com',
|
|
289
|
+
* flag: 0x41,
|
|
290
|
+
* credential: { id: new Uint8Array(32), publicKey },
|
|
291
|
+
* }),
|
|
292
|
+
* })
|
|
293
|
+
* ```
|
|
294
|
+
*
|
|
295
|
+
* @param options - Options to construct the attestation object.
|
|
296
|
+
* @returns The CBOR-encoded attestation object as a Hex string.
|
|
297
|
+
*/
|
|
298
|
+
export function getAttestationObject(
|
|
299
|
+
options: getAttestationObject.Options,
|
|
300
|
+
): Hex.Hex {
|
|
301
|
+
const { attStmt = {}, authData, fmt = 'none' } = options
|
|
302
|
+
return Cbor.encode({
|
|
303
|
+
fmt,
|
|
304
|
+
attStmt,
|
|
305
|
+
authData: Hex.toBytes(authData),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export declare namespace getAttestationObject {
|
|
310
|
+
type Options = {
|
|
311
|
+
/** Attestation statement. */
|
|
312
|
+
attStmt?: Record<string, unknown> | undefined
|
|
313
|
+
/** Authenticator data as a Hex string (from {@link ox#WebAuthnP256.(getAuthenticatorData:function)}). */
|
|
314
|
+
authData: Hex.Hex
|
|
315
|
+
/** Attestation format. @default 'none' */
|
|
316
|
+
fmt?: string | undefined
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
type ErrorType = Cbor.encode.ErrorType | Errors.GlobalErrorType
|
|
320
|
+
}
|
|
321
|
+
|
|
216
322
|
/**
|
|
217
323
|
* Returns the creation options for a P256 WebAuthn Credential to be used with
|
|
218
324
|
* the Web Authentication API.
|
|
@@ -716,7 +822,10 @@ export function verify(options: verify.Options): boolean {
|
|
|
716
822
|
// Check that response is for an authentication assertion (if typeIndex is provided)
|
|
717
823
|
if (typeIndex !== undefined) {
|
|
718
824
|
const type = '"type":"webauthn.get"'
|
|
719
|
-
if (
|
|
825
|
+
if (
|
|
826
|
+
type !==
|
|
827
|
+
clientDataJSON.slice(Number(typeIndex), Number(typeIndex) + type.length)
|
|
828
|
+
)
|
|
720
829
|
return false
|
|
721
830
|
}
|
|
722
831
|
|
package/index.ts
CHANGED
|
@@ -1330,7 +1330,6 @@ export * as Caches from './core/Caches.js'
|
|
|
1330
1330
|
* @category Data
|
|
1331
1331
|
*/
|
|
1332
1332
|
export * as Cbor from './core/Cbor.js'
|
|
1333
|
-
|
|
1334
1333
|
/**
|
|
1335
1334
|
* Utility functions for computing Contract Addresses.
|
|
1336
1335
|
*
|
|
@@ -1368,6 +1367,38 @@ export * as Cbor from './core/Cbor.js'
|
|
|
1368
1367
|
* @category Addresses
|
|
1369
1368
|
*/
|
|
1370
1369
|
export * as ContractAddress from './core/ContractAddress.js'
|
|
1370
|
+
/**
|
|
1371
|
+
* Utility functions for converting between COSE_Key and P256 public keys.
|
|
1372
|
+
*
|
|
1373
|
+
* COSE_Key is the key format used in WebAuthn attestation objects, as defined in
|
|
1374
|
+
* [RFC 9053](https://datatracker.ietf.org/doc/html/rfc9053).
|
|
1375
|
+
*
|
|
1376
|
+
* @example
|
|
1377
|
+
* ### Encoding a Public Key to COSE_Key
|
|
1378
|
+
*
|
|
1379
|
+
* ```ts twoslash
|
|
1380
|
+
* import { CoseKey, P256 } from 'ox'
|
|
1381
|
+
*
|
|
1382
|
+
* const { publicKey } = P256.createKeyPair()
|
|
1383
|
+
*
|
|
1384
|
+
* const coseKey = CoseKey.fromPublicKey(publicKey)
|
|
1385
|
+
* ```
|
|
1386
|
+
*
|
|
1387
|
+
* @example
|
|
1388
|
+
* ### Decoding a COSE_Key to Public Key
|
|
1389
|
+
*
|
|
1390
|
+
* ```ts twoslash
|
|
1391
|
+
* import { CoseKey, P256 } from 'ox'
|
|
1392
|
+
*
|
|
1393
|
+
* const { publicKey } = P256.createKeyPair()
|
|
1394
|
+
* const coseKey = CoseKey.fromPublicKey(publicKey)
|
|
1395
|
+
*
|
|
1396
|
+
* const publicKey2 = CoseKey.toPublicKey(coseKey)
|
|
1397
|
+
* ```
|
|
1398
|
+
*
|
|
1399
|
+
* @category Crypto
|
|
1400
|
+
*/
|
|
1401
|
+
export * as CoseKey from './core/CoseKey.js'
|
|
1371
1402
|
|
|
1372
1403
|
/**
|
|
1373
1404
|
* Utilities for working with Ed25519 signatures and key pairs.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ox",
|
|
3
3
|
"description": "Ethereum Standard Library",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.3",
|
|
5
5
|
"main": "./_cjs/index.js",
|
|
6
6
|
"module": "./_esm/index.js",
|
|
7
7
|
"types": "./_types/index.d.ts",
|
|
@@ -178,6 +178,11 @@
|
|
|
178
178
|
"import": "./_esm/core/ContractAddress.js",
|
|
179
179
|
"default": "./_cjs/core/ContractAddress.js"
|
|
180
180
|
},
|
|
181
|
+
"./CoseKey": {
|
|
182
|
+
"types": "./_types/core/CoseKey.d.ts",
|
|
183
|
+
"import": "./_esm/core/CoseKey.js",
|
|
184
|
+
"default": "./_cjs/core/CoseKey.js"
|
|
185
|
+
},
|
|
181
186
|
"./Ed25519": {
|
|
182
187
|
"types": "./_types/core/Ed25519.d.ts",
|
|
183
188
|
"import": "./_esm/core/Ed25519.js",
|
package/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export const version = '0.12.
|
|
2
|
+
export const version = '0.12.3'
|