@sockethub/crypto 1.0.0-alpha.3 → 1.0.0-alpha.5
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/package.json +14 -41
- package/src/index.test.ts +69 -61
- package/src/index.ts +76 -47
- package/dist/index.d.ts +0 -11
- package/dist/index.js +0 -49
- package/dist/index.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sockethub/crypto",
|
|
3
3
|
"description": "Crypto functions and helpers for Sockethub",
|
|
4
|
-
"version": "1.0.0-alpha.
|
|
4
|
+
"version": "1.0.0-alpha.5",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"private": false,
|
|
6
7
|
"author": "Nick Jennings <nick@silverbucket.net>",
|
|
7
8
|
"license": "MIT",
|
|
8
|
-
"main": "
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"src/"
|
|
13
|
-
],
|
|
9
|
+
"main": "src/index.ts",
|
|
10
|
+
"engines": {
|
|
11
|
+
"bun": ">=1.2"
|
|
12
|
+
},
|
|
14
13
|
"keywords": [
|
|
15
14
|
"sockethub",
|
|
16
15
|
"messaging",
|
|
@@ -26,41 +25,15 @@
|
|
|
26
25
|
},
|
|
27
26
|
"homepage": "https://github.com/sockethub/sockethub/tree/master/packages/data-layer",
|
|
28
27
|
"dependencies": {
|
|
29
|
-
"@sockethub/schemas": "
|
|
30
|
-
"object-hash": "
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"clean": "npx rimraf dist coverage",
|
|
34
|
-
"clean:deps": "npx rimraf node_modules",
|
|
35
|
-
"compliance": "yarn run lint && yarn run test && yarn run coverage",
|
|
36
|
-
"test": "c8 -x \"src/**/*.test.*\" mocha -r ts-node/register src/*.test.ts",
|
|
37
|
-
"coverage": "c8 check-coverage --statements 100 --branches 90 --functions 85 --lines 100",
|
|
38
|
-
"lint": "eslint \"**/*.ts\"",
|
|
39
|
-
"lint:fix": "eslint --fix \"**/*.ts\"",
|
|
40
|
-
"build": "tsc"
|
|
41
|
-
},
|
|
42
|
-
"engines": {
|
|
43
|
-
"node": ">= 14"
|
|
28
|
+
"@sockethub/schemas": "3.0.0-alpha.5",
|
|
29
|
+
"object-hash": "3.0.0"
|
|
44
30
|
},
|
|
45
31
|
"devDependencies": {
|
|
46
|
-
"@types/
|
|
47
|
-
"@types/debug": "4.1.
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/
|
|
50
|
-
"
|
|
51
|
-
"@types/object-hash": "2.2.1",
|
|
52
|
-
"@types/proxyquire": "1.3.28",
|
|
53
|
-
"@types/sinon": "10.0.13",
|
|
54
|
-
"@typescript-eslint/parser": "5.37.0",
|
|
55
|
-
"c8": "7.12.0",
|
|
56
|
-
"chai": "4.3.6",
|
|
57
|
-
"eslint": "8.23.1",
|
|
58
|
-
"eslint-cli": "1.1.1",
|
|
59
|
-
"mocha": "10.0.0",
|
|
60
|
-
"proxyquire": "2.1.3",
|
|
61
|
-
"sinon": "14.0.0",
|
|
62
|
-
"ts-node": "10.9.1",
|
|
63
|
-
"typescript": "4.8.3"
|
|
32
|
+
"@types/bun": "latest",
|
|
33
|
+
"@types/debug": "4.1.12",
|
|
34
|
+
"@types/object-hash": "3.0.6",
|
|
35
|
+
"@types/sinon": "17.0.2",
|
|
36
|
+
"sinon": "17.0.1"
|
|
64
37
|
},
|
|
65
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "341ea9eeca6afd1442fe6e01457bc21d112b91a4"
|
|
66
39
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,78 +1,86 @@
|
|
|
1
|
-
import { expect } from
|
|
2
|
-
import proxyquire from 'proxyquire';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
3
2
|
import * as sinon from "sinon";
|
|
4
3
|
|
|
5
|
-
import
|
|
4
|
+
import { Crypto, getPlatformId } from "./index";
|
|
6
5
|
|
|
7
|
-
const secret =
|
|
8
|
-
const data = {
|
|
9
|
-
const encryptedData =
|
|
6
|
+
const secret = "a test secret.. that is 16 x 2..";
|
|
7
|
+
const data = { foo: "bar" };
|
|
8
|
+
const encryptedData =
|
|
9
|
+
"00000000000000000000000000000000:0543ec94d863fbf4b7a19b48e69d9317";
|
|
10
10
|
|
|
11
|
-
describe(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
describe("crypto", () => {
|
|
12
|
+
let crypto;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
class TestCrypto extends Crypto {
|
|
15
|
+
createRandomBytes() {
|
|
16
|
+
this.randomBytes = () => Buffer.alloc(16);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
crypto = new TestCrypto();
|
|
20
|
+
});
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
it("encrypts", () => {
|
|
23
|
+
expect(crypto.encrypt(data, secret)).toEqual(encryptedData);
|
|
24
|
+
});
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
it("decrypts", () => {
|
|
27
|
+
expect(crypto.decrypt(encryptedData, secret)).toEqual(data);
|
|
28
|
+
});
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
it("hashes", () => {
|
|
31
|
+
expect(crypto.hash("foobar")).toEqual("8843d7f");
|
|
32
|
+
});
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
it("randTokens 8", () => {
|
|
35
|
+
const token = crypto.randToken(8);
|
|
36
|
+
expect(token.length).toEqual(8);
|
|
37
|
+
});
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
it("randTokens 16", () => {
|
|
40
|
+
const token = crypto.randToken(16);
|
|
41
|
+
expect(token.length).toEqual(16);
|
|
42
|
+
});
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
it("randTokens 32", () => {
|
|
45
|
+
const token = crypto.randToken(32);
|
|
46
|
+
expect(token.length).toEqual(32);
|
|
47
|
+
});
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
it("randTokens 33+ will fail", () => {
|
|
50
|
+
expect(() => {
|
|
51
|
+
crypto.randToken(33);
|
|
52
|
+
}).toThrow();
|
|
53
|
+
});
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
describe("getPlatformId", () => {
|
|
56
|
-
|
|
57
|
+
let cryptoHashStub: any;
|
|
58
|
+
let crypto;
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
cryptoHashStub = sinon.mock();
|
|
62
|
+
class TestCrypto extends Crypto {
|
|
63
|
+
createRandomBytes() {
|
|
64
|
+
this.randomBytes = () => Buffer.alloc(16);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
crypto = new TestCrypto();
|
|
68
|
+
cryptoHashStub = sinon.stub(crypto, "hash");
|
|
69
|
+
cryptoHashStub.returnsArg(0);
|
|
70
|
+
});
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
cryptoHashStub.restore();
|
|
74
|
+
});
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
76
|
+
it("generates platform hash", () => {
|
|
77
|
+
expect(getPlatformId("foo", undefined, crypto)).toEqual("foo");
|
|
78
|
+
sinon.assert.calledOnce(cryptoHashStub);
|
|
79
|
+
sinon.assert.calledWith(cryptoHashStub, "foo");
|
|
80
|
+
});
|
|
81
|
+
it("generates platform + actor hash", () => {
|
|
82
|
+
expect(getPlatformId("foo", "bar", crypto)).toEqual("foobar");
|
|
83
|
+
sinon.assert.calledOnce(cryptoHashStub);
|
|
84
|
+
sinon.assert.calledWith(cryptoHashStub, "foobar");
|
|
85
|
+
});
|
|
86
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,53 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
randomBytes,
|
|
6
|
+
} from "node:crypto";
|
|
7
|
+
import type { ActivityStream } from "@sockethub/schemas";
|
|
8
|
+
import hash from "object-hash";
|
|
4
9
|
|
|
5
|
-
const ALGORITHM =
|
|
6
|
-
|
|
10
|
+
const ALGORITHM = "aes-256-cbc";
|
|
11
|
+
const IV_LENGTH = 16; // For AES, this is always 16
|
|
7
12
|
|
|
8
|
-
export function getPlatformId(
|
|
9
|
-
|
|
13
|
+
export function getPlatformId(
|
|
14
|
+
platform: string,
|
|
15
|
+
actor?: string,
|
|
16
|
+
_crypto = crypto,
|
|
17
|
+
): string {
|
|
18
|
+
return actor ? _crypto.hash(platform + actor) : _crypto.hash(platform);
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
class Crypto {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
21
|
+
export class Crypto {
|
|
22
|
+
randomBytes: typeof randomBytes;
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
this.createRandomBytes();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
createRandomBytes() {
|
|
29
|
+
this.randomBytes = randomBytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
encrypt(json: ActivityStream, secret: string): string {
|
|
33
|
+
Crypto.ensureSecret(secret);
|
|
34
|
+
const iv = this.randomBytes(IV_LENGTH);
|
|
35
|
+
const cipher = createCipheriv(ALGORITHM, Buffer.from(secret), iv);
|
|
36
|
+
let encrypted = cipher.update(JSON.stringify(json));
|
|
37
|
+
|
|
38
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
39
|
+
return `${iv.toString("hex")}:${encrypted.toString("hex")}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
decrypt(text: string, secret: string): ActivityStream {
|
|
43
|
+
Crypto.ensureSecret(secret);
|
|
44
|
+
const parts = text.split(":");
|
|
45
|
+
const iv = Buffer.from(parts.shift(), "hex");
|
|
46
|
+
const encryptedText = Buffer.from(parts.join(":"), "hex");
|
|
47
|
+
const decipher = createDecipheriv(ALGORITHM, Buffer.from(secret), iv);
|
|
48
|
+
let decrypted = decipher.update(encryptedText);
|
|
49
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
50
|
+
return JSON.parse(decrypted.toString());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
hash(text: string): string {
|
|
54
|
+
const SHASum = createHash("sha1");
|
|
55
|
+
SHASum.update(text);
|
|
56
|
+
return SHASum.digest("hex").substring(0, 7);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
objectHash(object: object): string {
|
|
60
|
+
return hash(object);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
randToken(len: number): string {
|
|
64
|
+
if (len > 32) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`crypto.randToken supports a length param of up to 32, ${len} given`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const buf = this.randomBytes(len);
|
|
70
|
+
return buf.toString("hex").substring(0, len);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static ensureSecret(secret: string) {
|
|
74
|
+
if (secret.length !== 32) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`secret must be a 32 char string, length: ${secret.length}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
50
80
|
}
|
|
51
81
|
|
|
52
|
-
const crypto = new Crypto();
|
|
53
|
-
export default crypto;
|
|
82
|
+
export const crypto = new Crypto();
|
package/dist/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { IActivityStream } from "@sockethub/schemas";
|
|
2
|
-
export declare function getPlatformId(platform: string, actor?: string): string;
|
|
3
|
-
declare class Crypto {
|
|
4
|
-
encrypt(json: IActivityStream, secret: string): string;
|
|
5
|
-
decrypt(text: string, secret: string): IActivityStream;
|
|
6
|
-
hash(text: string): string;
|
|
7
|
-
objectHash(object: any): string;
|
|
8
|
-
randToken(len: number): string;
|
|
9
|
-
}
|
|
10
|
-
declare const crypto: Crypto;
|
|
11
|
-
export default crypto;
|
package/dist/index.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getPlatformId = void 0;
|
|
7
|
-
const crypto_1 = require("crypto");
|
|
8
|
-
const object_hash_1 = __importDefault(require("object-hash"));
|
|
9
|
-
const ALGORITHM = 'aes-256-cbc', IV_LENGTH = 16; // For AES, this is always 16
|
|
10
|
-
function getPlatformId(platform, actor) {
|
|
11
|
-
return actor ? crypto.hash(platform + actor) : crypto.hash(platform);
|
|
12
|
-
}
|
|
13
|
-
exports.getPlatformId = getPlatformId;
|
|
14
|
-
class Crypto {
|
|
15
|
-
encrypt(json, secret) {
|
|
16
|
-
const iv = (0, crypto_1.randomBytes)(IV_LENGTH);
|
|
17
|
-
const cipher = (0, crypto_1.createCipheriv)(ALGORITHM, Buffer.from(secret), iv);
|
|
18
|
-
let encrypted = cipher.update(JSON.stringify(json));
|
|
19
|
-
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
20
|
-
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
21
|
-
}
|
|
22
|
-
decrypt(text, secret) {
|
|
23
|
-
const parts = text.split(':');
|
|
24
|
-
const iv = Buffer.from(parts.shift(), 'hex');
|
|
25
|
-
const encryptedText = Buffer.from(parts.join(':'), 'hex');
|
|
26
|
-
const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, Buffer.from(secret), iv);
|
|
27
|
-
let decrypted = decipher.update(encryptedText);
|
|
28
|
-
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
29
|
-
return JSON.parse(decrypted.toString());
|
|
30
|
-
}
|
|
31
|
-
hash(text) {
|
|
32
|
-
const SHASum = (0, crypto_1.createHash)('sha1');
|
|
33
|
-
SHASum.update(text);
|
|
34
|
-
return SHASum.digest('hex').substring(0, 7);
|
|
35
|
-
}
|
|
36
|
-
objectHash(object) {
|
|
37
|
-
return (0, object_hash_1.default)(object);
|
|
38
|
-
}
|
|
39
|
-
randToken(len) {
|
|
40
|
-
if (len > 32) {
|
|
41
|
-
throw new Error(`crypto.randToken supports a length param of up to 32, ${len} given`);
|
|
42
|
-
}
|
|
43
|
-
const buf = (0, crypto_1.randomBytes)(len);
|
|
44
|
-
return buf.toString('hex').substring(0, len);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const crypto = new Crypto();
|
|
48
|
-
exports.default = crypto;
|
|
49
|
-
//# sourceMappingURL=/index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":";;;;;;AAAA,mCAAmF;AAEnF,8DAA+B;AAE/B,MAAM,SAAS,GAAG,aAAa,EACzB,SAAS,GAAG,EAAE,CAAC,CAAC,6BAA6B;AAEnD,SAAgB,aAAa,CAAC,QAAgB,EAAE,KAAc;IAC5D,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACvE,CAAC;AAFD,sCAEC;AAED,MAAM,MAAM;IAEV,OAAO,CAAC,IAAqB,EAAE,MAAc;QAC3C,MAAM,EAAE,GAAG,IAAA,oBAAW,EAAC,SAAS,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,IAAA,uBAAc,EAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAClE,IAAI,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAEpD,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACvD,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,CAAC,IAAY,EAAE,MAAc;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC;QAC7C,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;QAC1D,MAAM,QAAQ,GAAG,IAAA,yBAAgB,EAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QACtE,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC/C,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,CAAC,IAAY;QACf,MAAM,MAAM,GAAG,IAAA,mBAAU,EAAC,MAAM,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,UAAU,CAAC,MAAW;QACpB,OAAO,IAAA,qBAAI,EAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAED,SAAS,CAAC,GAAW;QACnB,IAAI,GAAG,GAAG,EAAE,EAAE;YACZ,MAAM,IAAI,KAAK,CAAC,yDAAyD,GAAG,QAAQ,CAAC,CAAC;SACvF;QACD,MAAM,GAAG,GAAG,IAAA,oBAAW,EAAC,GAAG,CAAC,CAAC;QAC7B,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/C,CAAC;CACF;AAED,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;AAC5B,kBAAe,MAAM,CAAC"}
|