node-red-contrib-nostr 0.1.3 → 0.2.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.
- package/dist/core/event.js +4 -4
- package/dist/crypto/keys.d.ts +2 -4
- package/dist/crypto/keys.js +27 -71
- package/dist/nips/nip04.js +7 -40
- package/dist/nodes/nostr-filter/nostr-filter.js +16 -2
- package/dist/nodes/nostr-relay-config/nostr-relay-config.d.ts +5 -1
- package/dist/nodes/nostr-relay-config/nostr-relay-config.js +23 -6
- package/package.json +9 -9
package/dist/core/event.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EventBuilder = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
5
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
6
6
|
const keys_1 = require("../crypto/keys");
|
|
7
7
|
class EventBuilder {
|
|
8
8
|
static async createEvent(kind, content, privateKey, tags = []) {
|
|
@@ -26,7 +26,7 @@ class EventBuilder {
|
|
|
26
26
|
event.tags,
|
|
27
27
|
event.content
|
|
28
28
|
]);
|
|
29
|
-
event.id = (0,
|
|
29
|
+
event.id = (0, utils_js_1.bytesToHex)((0, sha2_js_1.sha256)(Buffer.from(serialized)));
|
|
30
30
|
// Sign the event
|
|
31
31
|
event.sig = await this.keyManager.sign(privateKey, event.id);
|
|
32
32
|
return event;
|
|
@@ -46,7 +46,7 @@ class EventBuilder {
|
|
|
46
46
|
event.tags,
|
|
47
47
|
event.content
|
|
48
48
|
]);
|
|
49
|
-
const id = (0,
|
|
49
|
+
const id = (0, utils_js_1.bytesToHex)((0, sha2_js_1.sha256)(Buffer.from(serialized)));
|
|
50
50
|
if (id !== event.id) {
|
|
51
51
|
return false;
|
|
52
52
|
}
|
package/dist/crypto/keys.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare function getDefaultReaderKeys(): Promise<{
|
|
2
2
|
privateKey: string;
|
|
3
3
|
publicKey: string;
|
|
4
|
-
}
|
|
4
|
+
}>;
|
|
5
5
|
/**
|
|
6
6
|
* Generate a new Nostr key pair
|
|
7
7
|
* @returns {Promise<Object>} Object containing private and public keys
|
|
@@ -29,8 +29,6 @@ export declare function npubToHex(npub: string): string;
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function getPublicKey(privateKeyHex: string): Promise<string>;
|
|
31
31
|
export declare class KeyManager {
|
|
32
|
-
private secp256k1Promise;
|
|
33
|
-
constructor();
|
|
34
32
|
generatePrivateKey(): Promise<string>;
|
|
35
33
|
getPublicKey(privateKey: string): Promise<string>;
|
|
36
34
|
sign(privateKey: string, message: string): Promise<string>;
|
package/dist/crypto/keys.js
CHANGED
|
@@ -1,70 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.KeyManager =
|
|
3
|
+
exports.KeyManager = void 0;
|
|
4
|
+
exports.getDefaultReaderKeys = getDefaultReaderKeys;
|
|
37
5
|
exports.generateKeyPair = generateKeyPair;
|
|
38
6
|
exports.hexToNpub = hexToNpub;
|
|
39
7
|
exports.npubToHex = npubToHex;
|
|
40
8
|
exports.getPublicKey = getPublicKey;
|
|
9
|
+
const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
|
|
41
10
|
const bech32_1 = require("bech32");
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let secp256k1;
|
|
51
|
-
async function initSecp256k1() {
|
|
52
|
-
if (!secp256k1) {
|
|
53
|
-
secp256k1 = await Promise.resolve().then(() => __importStar(require('@noble/secp256k1')));
|
|
11
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
12
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
13
|
+
// Lazily-initialized ephemeral key pair for read-only operations.
|
|
14
|
+
// Generated at first use so no secret material is stored in source code.
|
|
15
|
+
let _defaultReaderKeys = null;
|
|
16
|
+
async function getDefaultReaderKeys() {
|
|
17
|
+
if (!_defaultReaderKeys) {
|
|
18
|
+
_defaultReaderKeys = await generateKeyPair();
|
|
54
19
|
}
|
|
55
|
-
return
|
|
20
|
+
return _defaultReaderKeys;
|
|
56
21
|
}
|
|
57
22
|
/**
|
|
58
23
|
* Generate a new Nostr key pair
|
|
59
24
|
* @returns {Promise<Object>} Object containing private and public keys
|
|
60
25
|
*/
|
|
61
26
|
async function generateKeyPair() {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const publicKey = secp.getPublicKey(privateKey, true);
|
|
27
|
+
const privateKey = secp256k1_js_1.secp256k1.utils.randomSecretKey();
|
|
28
|
+
const publicKey = secp256k1_js_1.secp256k1.getPublicKey(privateKey, true);
|
|
65
29
|
return {
|
|
66
|
-
privateKey:
|
|
67
|
-
publicKey:
|
|
30
|
+
privateKey: (0, utils_js_1.bytesToHex)(privateKey),
|
|
31
|
+
publicKey: (0, utils_js_1.bytesToHex)(publicKey)
|
|
68
32
|
};
|
|
69
33
|
}
|
|
70
34
|
/**
|
|
@@ -91,34 +55,26 @@ function npubToHex(npub) {
|
|
|
91
55
|
* @returns {Promise<string>} Public key in hex format
|
|
92
56
|
*/
|
|
93
57
|
async function getPublicKey(privateKeyHex) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
return Buffer.from(publicKey).toString('hex');
|
|
58
|
+
const publicKey = secp256k1_js_1.secp256k1.getPublicKey((0, utils_js_1.hexToBytes)(privateKeyHex), true);
|
|
59
|
+
return (0, utils_js_1.bytesToHex)(publicKey);
|
|
97
60
|
}
|
|
98
61
|
class KeyManager {
|
|
99
|
-
constructor() {
|
|
100
|
-
this.secp256k1Promise = initSecp256k1();
|
|
101
|
-
}
|
|
102
62
|
async generatePrivateKey() {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
return Buffer.from(privateKey).toString('hex');
|
|
63
|
+
const privateKey = secp256k1_js_1.secp256k1.utils.randomSecretKey();
|
|
64
|
+
return (0, utils_js_1.bytesToHex)(privateKey);
|
|
106
65
|
}
|
|
107
66
|
async getPublicKey(privateKey) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
return Buffer.from(publicKey).toString('hex');
|
|
67
|
+
const publicKey = secp256k1_js_1.secp256k1.getPublicKey((0, utils_js_1.hexToBytes)(privateKey), true);
|
|
68
|
+
return (0, utils_js_1.bytesToHex)(publicKey);
|
|
111
69
|
}
|
|
112
70
|
async sign(privateKey, message) {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
return (0, utils_1.bytesToHex)(signature.toCompactRawBytes());
|
|
71
|
+
const messageHash = (0, sha2_js_1.sha256)(new TextEncoder().encode(message));
|
|
72
|
+
const signature = secp256k1_js_1.secp256k1.sign(messageHash, (0, utils_js_1.hexToBytes)(privateKey));
|
|
73
|
+
return (0, utils_js_1.bytesToHex)(signature);
|
|
117
74
|
}
|
|
118
75
|
async verify(publicKey, message, signature) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
return secp.verify(signature, messageHash, publicKey);
|
|
76
|
+
const messageHash = (0, sha2_js_1.sha256)(new TextEncoder().encode(message));
|
|
77
|
+
return secp256k1_js_1.secp256k1.verify((0, utils_js_1.hexToBytes)(signature), messageHash, (0, utils_js_1.hexToBytes)(publicKey));
|
|
122
78
|
}
|
|
123
79
|
}
|
|
124
80
|
exports.KeyManager = KeyManager;
|
package/dist/nips/nip04.js
CHANGED
|
@@ -1,55 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.NIP04 = void 0;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
4
|
+
const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
|
|
5
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
6
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
40
7
|
const event_1 = require("../core/event");
|
|
41
8
|
class NIP04 {
|
|
42
9
|
static async encrypt(privateKey, publicKey, _text) {
|
|
43
|
-
const sharedPoint = secp256k1.getSharedSecret((0,
|
|
10
|
+
const sharedPoint = secp256k1_js_1.secp256k1.getSharedSecret((0, utils_js_1.hexToBytes)(privateKey), (0, utils_js_1.hexToBytes)(publicKey));
|
|
44
11
|
const sharedX = sharedPoint.slice(1, 33);
|
|
45
12
|
// In a real implementation, we'd use this shared secret with
|
|
46
13
|
// proper encryption. This is just a placeholder.
|
|
47
|
-
const key = (0,
|
|
14
|
+
const key = (0, sha2_js_1.sha256)(sharedX);
|
|
48
15
|
// TODO: Implement actual encryption using AES-256-CBC
|
|
49
|
-
return (0,
|
|
16
|
+
return (0, utils_js_1.bytesToHex)(key);
|
|
50
17
|
}
|
|
51
18
|
static async decrypt(privateKey, publicKey, _encryptedText) {
|
|
52
|
-
const sharedPoint = secp256k1.getSharedSecret((0,
|
|
19
|
+
const sharedPoint = secp256k1_js_1.secp256k1.getSharedSecret((0, utils_js_1.hexToBytes)(privateKey), (0, utils_js_1.hexToBytes)(publicKey));
|
|
53
20
|
const _sharedX = sharedPoint.slice(1, 33);
|
|
54
21
|
// TODO: Implement actual decryption using AES-256-CBC
|
|
55
22
|
return "decrypted text";
|
|
@@ -34,6 +34,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.default = default_1;
|
|
37
|
+
// Allowed fields in a Nostr subscription filter (NIP-01 + NIP-12 tag filters)
|
|
38
|
+
const ALLOWED_FILTER_FIELDS = new Set(['ids', 'authors', 'kinds', 'since', 'until', 'limit', 'search']);
|
|
39
|
+
function sanitizeFilterObject(parsed) {
|
|
40
|
+
const validated = {};
|
|
41
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
42
|
+
if (ALLOWED_FILTER_FIELDS.has(key) || key.startsWith('#')) {
|
|
43
|
+
validated[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return validated;
|
|
47
|
+
}
|
|
37
48
|
function default_1(RED) {
|
|
38
49
|
// Create a function to initialize the node
|
|
39
50
|
async function initializeNode(config) {
|
|
@@ -103,7 +114,8 @@ function default_1(RED) {
|
|
|
103
114
|
case 'custom':
|
|
104
115
|
if (this.customFilter) {
|
|
105
116
|
try {
|
|
106
|
-
const
|
|
117
|
+
const parsed = JSON.parse(this.customFilter);
|
|
118
|
+
const filter = sanitizeFilterObject(parsed);
|
|
107
119
|
shouldForward = Object.entries(filter).every(([key, value]) => {
|
|
108
120
|
if (Array.isArray(value)) {
|
|
109
121
|
return value.includes(event[key]);
|
|
@@ -147,7 +159,9 @@ function default_1(RED) {
|
|
|
147
159
|
case 'custom':
|
|
148
160
|
if (this.customFilter) {
|
|
149
161
|
try {
|
|
150
|
-
|
|
162
|
+
const parsed = JSON.parse(this.customFilter);
|
|
163
|
+
const validated = sanitizeFilterObject(parsed);
|
|
164
|
+
Object.assign(filter, validated);
|
|
151
165
|
}
|
|
152
166
|
catch (err) {
|
|
153
167
|
this.error("Invalid custom filter: " + err.message);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Node, NodeAPI } from 'node-red';
|
|
2
2
|
import type { NostrWSClient } from 'nostr-websocket-utils';
|
|
3
|
-
|
|
3
|
+
interface NostrRelayConfigCredentials {
|
|
4
|
+
privateKey?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface NostrRelayConfig extends Node<NostrRelayConfigCredentials> {
|
|
4
7
|
relay: string;
|
|
5
8
|
publicKey?: string;
|
|
6
9
|
privateKey?: string;
|
|
7
10
|
_ws?: NostrWSClient;
|
|
8
11
|
}
|
|
9
12
|
export default function (RED: NodeAPI): void;
|
|
13
|
+
export {};
|
|
@@ -35,13 +35,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.default = default_1;
|
|
37
37
|
const keys_js_1 = require("../../crypto/keys.js");
|
|
38
|
+
const validation_js_1 = require("../../utils/validation.js");
|
|
38
39
|
function default_1(RED) {
|
|
39
40
|
// Create a function to initialize the node
|
|
40
41
|
async function initializeNode(config) {
|
|
41
42
|
RED.nodes.createNode(this, config);
|
|
42
|
-
|
|
43
|
+
// Validate relay URL before accepting
|
|
44
|
+
if (config.relay) {
|
|
45
|
+
if (!validation_js_1.Validation.isValidRelayUrl(config.relay)) {
|
|
46
|
+
this.error('Invalid relay URL: must use ws:// or wss:// protocol and be a valid URL');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.relay = config.relay;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.error('Relay URL is required');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
43
55
|
this.publicKey = config.publicKey;
|
|
44
|
-
this.privateKey =
|
|
56
|
+
this.privateKey = this.credentials?.privateKey;
|
|
45
57
|
// Set up keys based on mode
|
|
46
58
|
if (this.publicKey && this.privateKey) {
|
|
47
59
|
try {
|
|
@@ -53,9 +65,10 @@ function default_1(RED) {
|
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
else {
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
this.
|
|
68
|
+
// Generate ephemeral reader keys
|
|
69
|
+
const readerKeys = await (0, keys_js_1.getDefaultReaderKeys)();
|
|
70
|
+
this.publicKey = readerKeys.publicKey;
|
|
71
|
+
this.privateKey = readerKeys.privateKey;
|
|
59
72
|
}
|
|
60
73
|
try {
|
|
61
74
|
// Dynamically import ESM dependency
|
|
@@ -92,10 +105,14 @@ function default_1(RED) {
|
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
// Register the node
|
|
95
|
-
RED.nodes.registerType("nostr-relay-config", function (config) {
|
|
108
|
+
RED.nodes.registerType("nostr-relay-config", (function (config) {
|
|
96
109
|
// Initialize asynchronously
|
|
97
110
|
initializeNode.call(this, config).catch((err) => {
|
|
98
111
|
this.error("Failed to initialize node: " + err.message);
|
|
99
112
|
});
|
|
113
|
+
}), {
|
|
114
|
+
credentials: {
|
|
115
|
+
privateKey: { type: "password" }
|
|
116
|
+
}
|
|
100
117
|
});
|
|
101
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-nostr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Node-RED nodes for seamless Nostr protocol integration. Features robust WebSocket handling, event filtering, and NPUB-based routing. Built with TypeScript for type safety and extensive testing. Perfect for Nostr automation flows.",
|
|
5
5
|
"author": "vveerrgg",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@noble/
|
|
53
|
-
"@noble/
|
|
52
|
+
"@noble/curves": "^2.0.1",
|
|
53
|
+
"@noble/hashes": "^2.0.1",
|
|
54
54
|
"bech32": "^2.0.0",
|
|
55
|
-
"nostr-tools": "^
|
|
56
|
-
"nostr-websocket-utils": "^0.
|
|
55
|
+
"nostr-tools": "^2.23.3",
|
|
56
|
+
"nostr-websocket-utils": "^0.4.0",
|
|
57
57
|
"ws": "^8.19.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
@@ -63,16 +63,16 @@
|
|
|
63
63
|
"@types/ws": "^8.18.1",
|
|
64
64
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
65
65
|
"@typescript-eslint/parser": "^8.56.0",
|
|
66
|
-
"@vitest/coverage-v8": "^
|
|
67
|
-
"@vitest/ui": "^
|
|
66
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
67
|
+
"@vitest/ui": "^4.0.18",
|
|
68
68
|
"eslint": "^10.0.1",
|
|
69
69
|
"node-red": "^4.1.5",
|
|
70
70
|
"node-red-node-test-helper": "^0.3.6",
|
|
71
71
|
"typescript": "^5.9.3",
|
|
72
|
-
"vitest": "^
|
|
72
|
+
"vitest": "^4.0.18"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
76
|
},
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|