@mh-gg/event 0.1.1-alpha.20260613T085325975Z

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 ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@mh-gg/event",
3
+ "version": "0.1.1-alpha.20260613T085325975Z",
4
+ "description": "Signed, encrypted party event protocol helpers for matterhorn.",
5
+ "type": "module",
6
+ "main": "src/index.cjs",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "import": "./src/index.js",
11
+ "require": "./src/index.cjs",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./canonicalJson": {
15
+ "types": "./src/canonicalJson.d.ts",
16
+ "import": "./src/canonicalJson.js",
17
+ "require": "./src/canonicalJson.cjs",
18
+ "default": "./src/canonicalJson.js"
19
+ },
20
+ "./matterhornOperationBuilder": {
21
+ "types": "./src/matterhornOperationBuilder.d.ts",
22
+ "import": "./src/matterhornOperationBuilder.js",
23
+ "require": "./src/matterhornOperationBuilder.cjs",
24
+ "default": "./src/matterhornOperationBuilder.js"
25
+ },
26
+ "./ngramIndex": {
27
+ "types": "./src/ngramIndex.d.ts",
28
+ "import": "./src/ngramIndex.js",
29
+ "require": "./src/ngramIndex.cjs",
30
+ "default": "./src/ngramIndex.js"
31
+ }
32
+ },
33
+ "typesVersions": {
34
+ "*": {
35
+ "canonicalJson": [
36
+ "src/canonicalJson.d.ts"
37
+ ],
38
+ "matterhornOperationBuilder": [
39
+ "src/matterhornOperationBuilder.d.ts"
40
+ ],
41
+ "*": [
42
+ "src/index.d.ts"
43
+ ],
44
+ "ngramIndex": [
45
+ "src/ngramIndex.d.ts"
46
+ ]
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "nostr-tools": "^2.23.5"
51
+ },
52
+ "engines": {
53
+ "node": ">=22.12"
54
+ },
55
+ "types": "src/index.d.ts",
56
+ "scripts": {
57
+ "test": "node --test test/*.test.cjs",
58
+ "coverage": "node --test --experimental-test-coverage --test-coverage-lines=80 --test-coverage-functions=80 --test-coverage-branches=80 --test-coverage-include=src/*.cjs test/*.test.cjs"
59
+ }
60
+ }
@@ -0,0 +1,63 @@
1
+ const MAX_CANONICAL_DEPTH = 64;
2
+
3
+ function maxDepthFromOptions(options = {}) {
4
+ if (Number.isInteger(options.maxDepth) && options.maxDepth >= 0) return options.maxDepth;
5
+ return MAX_CANONICAL_DEPTH;
6
+ }
7
+
8
+ function canonicalJson(value, options = {}) {
9
+ return canonicalJsonInner(value, new WeakSet(), 0, maxDepthFromOptions(options));
10
+ }
11
+
12
+ function requireDepth(depth, maxDepth) {
13
+ if (depth > maxDepth) throw new Error("Cannot canonicalize: max depth exceeded");
14
+ }
15
+
16
+ function canonicalJsonInner(value, seen, depth, maxDepth) {
17
+ requireDepth(depth, maxDepth);
18
+
19
+ if (value === null) return "null";
20
+ if (typeof value === "string") return JSON.stringify(value.normalize("NFC"));
21
+ if (typeof value === "number") {
22
+ if (!Number.isFinite(value)) throw new Error("Invalid number");
23
+ return JSON.stringify(value);
24
+ }
25
+ if (typeof value === "boolean") return value ? "true" : "false";
26
+
27
+ if (Array.isArray(value)) {
28
+ if (seen.has(value)) throw new Error("Cannot canonicalize cyclic values");
29
+ seen.add(value);
30
+ try {
31
+ return `[${value.map((item) => canonicalJsonInner(item, seen, depth + 1, maxDepth)).join(",")}]`;
32
+ } finally {
33
+ seen.delete(value);
34
+ }
35
+ }
36
+
37
+ if (typeof value === "object") {
38
+ if (seen.has(value)) throw new Error("Cannot canonicalize cyclic values");
39
+ seen.add(value);
40
+ try {
41
+ const entries = [];
42
+ const normalizedKeys = new Set();
43
+ for (const key of Object.keys(value)) {
44
+ if (value[key] === undefined) continue;
45
+ const normalizedKey = key.normalize("NFC");
46
+ if (normalizedKeys.has(normalizedKey)) throw new Error("Cannot canonicalize duplicate normalized object keys");
47
+ normalizedKeys.add(normalizedKey);
48
+ entries.push([normalizedKey, value[key]]);
49
+ }
50
+ entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
51
+ return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJsonInner(entryValue, seen, depth + 1, maxDepth)}`).join(",")}}`;
52
+ } finally {
53
+ seen.delete(value);
54
+ }
55
+ }
56
+
57
+ throw new Error("Unsupported value");
58
+ }
59
+
60
+ module.exports = {
61
+ MAX_CANONICAL_DEPTH,
62
+ canonicalJson
63
+ };
@@ -0,0 +1,2 @@
1
+ export const MAX_CANONICAL_DEPTH: number;
2
+ export function canonicalJson(value: unknown, options?: { maxDepth?: number }): string;
@@ -0,0 +1,58 @@
1
+ export const MAX_CANONICAL_DEPTH = 64;
2
+
3
+ function maxDepthFromOptions(options = {}) {
4
+ if (Number.isInteger(options.maxDepth) && options.maxDepth >= 0) return options.maxDepth;
5
+ return MAX_CANONICAL_DEPTH;
6
+ }
7
+
8
+ export function canonicalJson(value, options = {}) {
9
+ return canonicalJsonInner(value, new WeakSet(), 0, maxDepthFromOptions(options));
10
+ }
11
+
12
+ function requireDepth(depth, maxDepth) {
13
+ if (depth > maxDepth) throw new Error("Cannot canonicalize: max depth exceeded");
14
+ }
15
+
16
+ function canonicalJsonInner(value, seen, depth, maxDepth) {
17
+ requireDepth(depth, maxDepth);
18
+
19
+ if (value === null) return "null";
20
+ if (typeof value === "string") return JSON.stringify(value.normalize("NFC"));
21
+ if (typeof value === "number") {
22
+ if (!Number.isFinite(value)) throw new Error("Invalid number");
23
+ return JSON.stringify(value);
24
+ }
25
+ if (typeof value === "boolean") return value ? "true" : "false";
26
+
27
+ if (Array.isArray(value)) {
28
+ if (seen.has(value)) throw new Error("Cannot canonicalize cyclic values");
29
+ seen.add(value);
30
+ try {
31
+ return `[${value.map((item) => canonicalJsonInner(item, seen, depth + 1, maxDepth)).join(",")}]`;
32
+ } finally {
33
+ seen.delete(value);
34
+ }
35
+ }
36
+
37
+ if (typeof value === "object") {
38
+ if (seen.has(value)) throw new Error("Cannot canonicalize cyclic values");
39
+ seen.add(value);
40
+ try {
41
+ const entries = [];
42
+ const normalizedKeys = new Set();
43
+ for (const key of Object.keys(value)) {
44
+ if (value[key] === undefined) continue;
45
+ const normalizedKey = key.normalize("NFC");
46
+ if (normalizedKeys.has(normalizedKey)) throw new Error("Cannot canonicalize duplicate normalized object keys");
47
+ normalizedKeys.add(normalizedKey);
48
+ entries.push([normalizedKey, value[key]]);
49
+ }
50
+ entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
51
+ return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJsonInner(entryValue, seen, depth + 1, maxDepth)}`).join(",")}}`;
52
+ } finally {
53
+ seen.delete(value);
54
+ }
55
+ }
56
+
57
+ throw new Error("Unsupported value");
58
+ }
package/src/chat.cjs ADDED
@@ -0,0 +1,28 @@
1
+ const { DEFAULT_MAX_MESSAGE_CHARS } = require("./constants.cjs");
2
+ const { createSignedPartyEvent } = require("./signing.cjs");
3
+ const { validateChatMessagePayload } = require("./validation.cjs");
4
+
5
+ function createChatMessageEvent(input) {
6
+ const payload = {
7
+ text: String(input.text || "")
8
+ };
9
+ if (input.displayName !== undefined) payload.displayName = String(input.displayName);
10
+ if (input.replyTo !== undefined) payload.replyTo = String(input.replyTo);
11
+
12
+ const validation = validateChatMessagePayload(payload, DEFAULT_MAX_MESSAGE_CHARS);
13
+ if (!validation.ok) throw new Error(validation.message);
14
+
15
+ return createSignedPartyEvent({
16
+ kind: "chat.message",
17
+ partyId: input.partyId,
18
+ identity: input.identity,
19
+ payload,
20
+ roomSecret: input.roomSecret,
21
+ epochId: input.epochId,
22
+ createdAt: input.createdAt
23
+ });
24
+ }
25
+
26
+ module.exports = {
27
+ createChatMessageEvent
28
+ };
@@ -0,0 +1,59 @@
1
+ const kinds = require("nostr-tools/kinds");
2
+
3
+ const PROTOCOL = "matterhorn-sdk";
4
+ const VERSION = 1;
5
+ const MATTERHORN_NOSTR_KIND_BY_EVENT_KIND = Object.freeze({
6
+ "party.created": kinds.Time,
7
+ "chat.message": kinds.ChannelMessage,
8
+ "chat.delete": kinds.EventDeletion,
9
+ "relay.announce": kinds.RelayList,
10
+ "room.operation": 9001,
11
+ "file.upload": 1063,
12
+ "push.home-relays": 9002,
13
+ "room.key.epoch": 9010,
14
+ "room.key.epoch.grant": 9011,
15
+ "room.index.key.grant": 9012,
16
+ "room.index.ngram": 9013
17
+ });
18
+ const MATTERHORN_EVENT_KIND_BY_NOSTR_KIND = Object.freeze(Object.fromEntries(
19
+ Object.entries(MATTERHORN_NOSTR_KIND_BY_EVENT_KIND).map(([eventKind, nostrKind]) => [nostrKind, eventKind])
20
+ ));
21
+ const MATTERHORN_NOSTR_KINDS = Object.freeze(Array.from(new Set(Object.values(MATTERHORN_NOSTR_KIND_BY_EVENT_KIND))));
22
+ const MATTERHORN_NOSTR_KIND = MATTERHORN_NOSTR_KIND_BY_EVENT_KIND["chat.message"];
23
+ const MATTERHORN_OPERATION_NOSTR_KIND = MATTERHORN_NOSTR_KIND_BY_EVENT_KIND["room.operation"];
24
+ const MATTERHORN_FILE_NOSTR_KIND = MATTERHORN_NOSTR_KIND_BY_EVENT_KIND["file.upload"];
25
+ const MATTERHORN_CALL_SIGNAL_NOSTR_KIND = 24242;
26
+ const MATTERHORN_PRESENCE_NOSTR_KIND = 24312;
27
+ const ENCRYPTION_ALG = "A256GCM";
28
+ const MATTERHORN_EVENT_KINDS = new Set(["party.created", "chat.message", "chat.delete", "relay.announce", "room.operation", "file.upload", "push.home-relays", "room.key.epoch", "room.key.epoch.grant", "room.index.key.grant", "room.index.ngram"]);
29
+ const MATTERHORN_PUSH_HOME_RELAYS_NOSTR_KIND = MATTERHORN_NOSTR_KIND_BY_EVENT_KIND["push.home-relays"];
30
+ const MATTERHORN_INDEX_NGRAM_NOSTR_KIND = MATTERHORN_NOSTR_KIND_BY_EVENT_KIND["room.index.ngram"];
31
+ const MATTERHORN_ID_PATTERN = /^[a-zA-Z0-9_-]{3,128}$/;
32
+ const HEX_32_PATTERN = /^[0-9a-f]{64}$/i;
33
+ const HEX_64_PATTERN = /^[0-9a-f]{128}$/i;
34
+ const DEFAULT_MAX_EVENT_BYTES = 8 * 1024;
35
+ const DEFAULT_MAX_MESSAGE_CHARS = 2000;
36
+ const DEFAULT_MAX_FUTURE_MS = 10 * 60 * 1000;
37
+
38
+ module.exports = {
39
+ DEFAULT_MAX_EVENT_BYTES,
40
+ DEFAULT_MAX_FUTURE_MS,
41
+ DEFAULT_MAX_MESSAGE_CHARS,
42
+ ENCRYPTION_ALG,
43
+ HEX_32_PATTERN,
44
+ HEX_64_PATTERN,
45
+ MATTERHORN_CALL_SIGNAL_NOSTR_KIND,
46
+ MATTERHORN_EVENT_KIND_BY_NOSTR_KIND,
47
+ MATTERHORN_EVENT_KINDS,
48
+ MATTERHORN_FILE_NOSTR_KIND,
49
+ MATTERHORN_INDEX_NGRAM_NOSTR_KIND,
50
+ MATTERHORN_ID_PATTERN,
51
+ MATTERHORN_NOSTR_KIND,
52
+ MATTERHORN_NOSTR_KIND_BY_EVENT_KIND,
53
+ MATTERHORN_NOSTR_KINDS,
54
+ MATTERHORN_OPERATION_NOSTR_KIND,
55
+ MATTERHORN_PUSH_HOME_RELAYS_NOSTR_KIND,
56
+ MATTERHORN_PRESENCE_NOSTR_KIND,
57
+ PROTOCOL,
58
+ VERSION
59
+ };
@@ -0,0 +1,19 @@
1
+ const crypto = require("node:crypto");
2
+
3
+ function sha256Hex(value) {
4
+ return crypto.createHash("sha256").update(value).digest("hex");
5
+ }
6
+
7
+ function base64url(bytes) {
8
+ return Buffer.from(bytes).toString("base64url");
9
+ }
10
+
11
+ function fromBase64url(value) {
12
+ return Buffer.from(String(value), "base64url");
13
+ }
14
+
15
+ module.exports = {
16
+ base64url,
17
+ fromBase64url,
18
+ sha256Hex
19
+ };
@@ -0,0 +1,141 @@
1
+ const crypto = require("node:crypto");
2
+ const { canonicalJson } = require("./canonicalJson.cjs");
3
+ const { ENCRYPTION_ALG, MATTERHORN_EVENT_KINDS, PROTOCOL, VERSION } = require("./constants.cjs");
4
+ const { base64url, fromBase64url } = require("./encoding.cjs");
5
+ const { isEncryptedPayload } = require("./payload.cjs");
6
+ const { isValidPartyId } = require("./ids.cjs");
7
+
8
+ const DERIVED_ROOM_KEY_CACHE_MAX = 256;
9
+ const derivedRoomKeyCache = new Map();
10
+
11
+ function derivedRoomKeyCacheKey(roomSecret, partyId, epochId) {
12
+ const secretHash = crypto.createHash("sha256").update(String(roomSecret)).digest("hex");
13
+ return `${secretHash}:${partyId}:${epochId || ""}`;
14
+ }
15
+
16
+ function setCachedRoomKey(cacheKey, keyBytes) {
17
+ if (derivedRoomKeyCache.has(cacheKey)) derivedRoomKeyCache.delete(cacheKey);
18
+ derivedRoomKeyCache.set(cacheKey, Buffer.from(keyBytes));
19
+ while (derivedRoomKeyCache.size > DERIVED_ROOM_KEY_CACHE_MAX) {
20
+ const oldestKey = derivedRoomKeyCache.keys().next().value;
21
+ derivedRoomKeyCache.delete(oldestKey);
22
+ }
23
+ }
24
+
25
+ function roomKeySalt(partyId, epochId) {
26
+ return epochId ? `matterhorn:${partyId}:epoch:${epochId}` : `matterhorn:${partyId}`;
27
+ }
28
+
29
+ function deriveRoomKeyBytes(roomSecret, partyId, epochId) {
30
+ const cacheKey = derivedRoomKeyCacheKey(roomSecret, partyId, epochId);
31
+ const cached = derivedRoomKeyCache.get(cacheKey);
32
+ if (cached) {
33
+ derivedRoomKeyCache.delete(cacheKey);
34
+ derivedRoomKeyCache.set(cacheKey, cached);
35
+ return Buffer.from(cached);
36
+ }
37
+ const keyBytes = crypto.pbkdf2Sync(
38
+ String(roomSecret),
39
+ roomKeySalt(partyId, epochId),
40
+ 120000,
41
+ 32,
42
+ "sha256"
43
+ );
44
+ setCachedRoomKey(cacheKey, keyBytes);
45
+ return Buffer.from(keyBytes);
46
+ }
47
+
48
+ function clearDerivedRoomKeyCache() {
49
+ derivedRoomKeyCache.clear();
50
+ }
51
+
52
+ function derivedRoomKeyCacheSize() {
53
+ return derivedRoomKeyCache.size;
54
+ }
55
+
56
+ function legacyEncryptionAad(partyId, kind, epochId) {
57
+ const suffix = epochId ? `:${epochId}` : "";
58
+ return Buffer.from(`${PROTOCOL}:v${VERSION}:${partyId}:${kind}${suffix}`, "utf8");
59
+ }
60
+
61
+ function headerEncryptionAad(header) {
62
+ if (!header || typeof header !== "object") {
63
+ throw new Error("header is required for room.operation v2 AAD");
64
+ }
65
+ // AAD excludes op-id and payload-hash to avoid circularity.
66
+ const aadHeader = {};
67
+ for (const [key, value] of Object.entries(header)) {
68
+ if (key === "opId" || key === "payloadHash") continue;
69
+ aadHeader[key] = value;
70
+ }
71
+ return Buffer.from(canonicalJson(aadHeader), "utf8");
72
+ }
73
+
74
+ function encryptionAad({ partyId, kind, epochId, header }) {
75
+ if (kind === "room.operation" && header) {
76
+ return headerEncryptionAad(header);
77
+ }
78
+ return legacyEncryptionAad(partyId, kind, epochId);
79
+ }
80
+
81
+ function encryptPartyPayload({ roomSecret, partyId, kind, payload, epochId, header }) {
82
+ if (!roomSecret) throw new Error("roomSecret is required to encrypt party payloads");
83
+ if (!isValidPartyId(partyId)) throw new Error("Invalid partyId");
84
+ if (!MATTERHORN_EVENT_KINDS.has(kind)) throw new Error("Invalid party event kind");
85
+
86
+ const iv = crypto.randomBytes(12);
87
+ const cipher = crypto.createCipheriv("aes-256-gcm", deriveRoomKeyBytes(roomSecret, partyId, epochId), iv);
88
+ cipher.setAAD(encryptionAad({ partyId, kind, epochId, header }));
89
+ const plaintext = Buffer.from(canonicalJson(payload), "utf8");
90
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
91
+
92
+ return {
93
+ encrypted: true,
94
+ alg: ENCRYPTION_ALG,
95
+ epochId,
96
+ iv: base64url(iv),
97
+ data: base64url(ciphertext)
98
+ };
99
+ }
100
+
101
+ function payloadHashFromCiphertext(dataBase64) {
102
+ const bytes = fromBase64url(dataBase64);
103
+ if (bytes.length < 16) return undefined;
104
+ const ciphertext = bytes.subarray(0, bytes.length - 16);
105
+ return `sha256:${crypto.createHash("sha256").update(ciphertext).digest("hex")}`;
106
+ }
107
+
108
+ function decryptPartyPayload({ roomSecret, partyId, kind, payload, header }) {
109
+ if (!isEncryptedPayload(payload)) throw new Error("Payload is not encrypted");
110
+
111
+ // For v2 operations, verify the ciphertext matches the header's payload-hash before decrypting.
112
+ if (kind === "room.operation" && header && header.scheme === "matterhorn.operation.v2" && header.payloadHash) {
113
+ const actualHash = payloadHashFromCiphertext(payload.data);
114
+ if (actualHash !== header.payloadHash) {
115
+ throw new Error("payload-hash mismatch: ciphertext does not match header");
116
+ }
117
+ }
118
+
119
+ const bytes = fromBase64url(payload.data);
120
+ if (bytes.length < 17) throw new Error("Invalid encrypted payload");
121
+
122
+ const ciphertext = bytes.subarray(0, bytes.length - 16);
123
+ const authTag = bytes.subarray(bytes.length - 16);
124
+ const decipher = crypto.createDecipheriv("aes-256-gcm", deriveRoomKeyBytes(roomSecret, partyId, payload.epochId), fromBase64url(payload.iv));
125
+ decipher.setAAD(encryptionAad({ partyId, kind, epochId: payload.epochId, header }));
126
+ decipher.setAuthTag(authTag);
127
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
128
+ return JSON.parse(plaintext.toString("utf8"));
129
+ }
130
+
131
+ module.exports = {
132
+ DERIVED_ROOM_KEY_CACHE_MAX,
133
+ clearDerivedRoomKeyCache,
134
+ decryptPartyPayload,
135
+ deriveRoomKeyBytes,
136
+ derivedRoomKeyCacheSize,
137
+ encryptPartyPayload,
138
+ encryptionAad,
139
+ headerEncryptionAad,
140
+ legacyEncryptionAad
141
+ };
@@ -0,0 +1,14 @@
1
+ const { isEncryptedPayload } = require("./payload.cjs");
2
+
3
+ function isPartyEventAllowedForKeyEpochs(event, keyEpochs = []) {
4
+ if (!isEncryptedPayload(event?.payload)) return true;
5
+ const epochId = event.payload.epochId;
6
+ if (!epochId) return true;
7
+ const epoch = keyEpochs.find((item) => item.id === epochId);
8
+ if (!epoch) return false;
9
+ return !epoch.retiredAt || event.createdAt <= epoch.retiredAt;
10
+ }
11
+
12
+ module.exports = {
13
+ isPartyEventAllowedForKeyEpochs
14
+ };
@@ -0,0 +1,16 @@
1
+ const { generateSecretKey, getPublicKey } = require("nostr-tools/pure");
2
+ const { bytesToHex } = require("nostr-tools/utils");
3
+
4
+ function generatePartyIdentity(createdAt = Date.now()) {
5
+ const privateKeyBytes = generateSecretKey();
6
+ const privateKey = bytesToHex(privateKeyBytes);
7
+ return {
8
+ pubkey: getPublicKey(privateKeyBytes),
9
+ privateKey,
10
+ createdAt
11
+ };
12
+ }
13
+
14
+ module.exports = {
15
+ generatePartyIdentity
16
+ };
package/src/ids.cjs ADDED
@@ -0,0 +1,9 @@
1
+ const { MATTERHORN_ID_PATTERN } = require("./constants.cjs");
2
+
3
+ function isValidPartyId(value) {
4
+ return typeof value === "string" && MATTERHORN_ID_PATTERN.test(value);
5
+ }
6
+
7
+ module.exports = {
8
+ isValidPartyId
9
+ };
package/src/index.cjs ADDED
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ ...require("./canonicalJson.cjs"),
3
+ ...require("./chat.cjs"),
4
+ ...require("./constants.cjs"),
5
+ ...require("./encoding.cjs"),
6
+ ...require("./encryption.cjs"),
7
+ ...require("./epochFilter.cjs"),
8
+ ...require("./identity.cjs"),
9
+ ...require("./ids.cjs"),
10
+ ...require("./nostrMapping.cjs"),
11
+ ...require("./ngramIndex.cjs"),
12
+ ...require("./payload.cjs"),
13
+ ...require("./relayPolicy.cjs"),
14
+ ...require("./signing.cjs"),
15
+ ...require("./validation.cjs")
16
+ };
package/src/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./canonicalJson";
2
+ export const MATTERHORN_CALL_SIGNAL_NOSTR_KIND: number;
3
+ export const MATTERHORN_FILE_NOSTR_KIND: number;
4
+ export const MATTERHORN_PRESENCE_NOSTR_KIND: number;
5
+ export const MATTERHORN_PUSH_HOME_RELAYS_NOSTR_KIND: number;
6
+ export const MATTERHORN_INDEX_NGRAM_NOSTR_KIND: number;
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { MAX_CANONICAL_DEPTH, canonicalJson } from "./canonicalJson.js";
2
+ export const MATTERHORN_CALL_SIGNAL_NOSTR_KIND = 24242;
3
+ export const MATTERHORN_FILE_NOSTR_KIND = 1063;
4
+ export const MATTERHORN_PRESENCE_NOSTR_KIND = 24312;
5
+ export const MATTERHORN_PUSH_HOME_RELAYS_NOSTR_KIND = 9002;
6
+ export const MATTERHORN_INDEX_NGRAM_NOSTR_KIND = 9013;
@@ -0,0 +1,101 @@
1
+ const {
2
+ createSignedPartyEvent,
3
+ partyEventToNostrEvent
4
+ } = require("./index.cjs");
5
+ const { createRoomIndexNgramEvents } = require("./ngramIndex.cjs");
6
+
7
+ function createMatterhornOperationEvent({ identity, operation, roomName, roomSecret, createdAt = Date.now() }) {
8
+ if (!roomSecret) throw new Error("roomSecret is required to create encrypted room operation events");
9
+
10
+ // Build the v2 plaintext header from the operation metadata.
11
+ const header = {
12
+ scheme: "matterhorn.operation.v2",
13
+ opId: operation.opId || operation.id,
14
+ hlc: operation.hlc,
15
+ plugin: operation.pluginId,
16
+ type: operation.type,
17
+ action: operation.schemaAction,
18
+ member: operation.actor?.memberId,
19
+ device: operation.actor?.deviceId,
20
+ role: operation.actor?.role,
21
+ grants: operation.grants,
22
+ epoch: operation.epoch,
23
+ stream: operation.stream,
24
+ seq: operation.seq,
25
+ appPackId: operation.appPackId,
26
+ appPackHash: operation.appPackHash
27
+ };
28
+
29
+ // Encrypt only the application payload.
30
+ const event = createSignedPartyEvent({
31
+ kind: "room.operation",
32
+ partyId: roomName,
33
+ identity,
34
+ header,
35
+ payload: operation.payload,
36
+ roomSecret,
37
+ epochId: operation.epoch,
38
+ createdAt
39
+ });
40
+ return partyEventToNostrEvent(event);
41
+ }
42
+
43
+ function tagValue(tags, name) {
44
+ if (!Array.isArray(tags)) return undefined;
45
+ const tag = tags.find((item) => Array.isArray(item) && item[0] === name && typeof item[1] === "string");
46
+ return tag?.[1];
47
+ }
48
+
49
+ function valueAtPath(value, path) {
50
+ if (!path || typeof path !== "string") return undefined;
51
+ let current = value;
52
+ for (const part of path.split(".")) {
53
+ if (!part) continue;
54
+ if (!current || typeof current !== "object") return undefined;
55
+ current = current[part];
56
+ }
57
+ return current;
58
+ }
59
+
60
+ function stringifySearchValue(value) {
61
+ if (value === undefined || value === null) return "";
62
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
63
+ if (Array.isArray(value)) return value.map(stringifySearchValue).filter(Boolean).join(" ");
64
+ return "";
65
+ }
66
+
67
+ function searchTextFromOptions(operation, options) {
68
+ if (Array.isArray(options.indexText)) return options.indexText.map(stringifySearchValue).filter(Boolean).join(" ");
69
+ if (options.indexText !== undefined) return stringifySearchValue(options.indexText);
70
+ if (Array.isArray(options.indexPaths)) {
71
+ return options.indexPaths
72
+ .map((path) => stringifySearchValue(valueAtPath(operation.payload, path)))
73
+ .filter(Boolean)
74
+ .join(" ");
75
+ }
76
+ return "";
77
+ }
78
+
79
+ function createMatterhornOperationEventBundle(options = {}) {
80
+ const event = createMatterhornOperationEvent(options);
81
+ const operation = options.operation || {};
82
+ const text = searchTextFromOptions(operation, options);
83
+ const indexEvents = options.roomIndexKey && text ? createRoomIndexNgramEvents({
84
+ identity: options.identity,
85
+ roomIndexKey: options.roomIndexKey,
86
+ roomName: options.roomName,
87
+ targetEventId: event.id,
88
+ targetOperationId: tagValue(event.tags, "op-id") || operation.opId || operation.id,
89
+ targetHlc: tagValue(event.tags, "hlc") || operation.hlc,
90
+ stream: tagValue(event.tags, "stream") || operation.stream,
91
+ field: Array.isArray(options.indexPaths) ? options.indexPaths.join(",") : options.indexField,
92
+ text,
93
+ minGram: options.minGram,
94
+ maxGram: options.maxGram,
95
+ maxTokensPerEvent: options.maxTokensPerEvent,
96
+ createdAt: (Number.isInteger(options.createdAt) ? options.createdAt : Date.now()) + 1
97
+ }) : [];
98
+ return { event, indexEvents, events: [event, ...indexEvents] };
99
+ }
100
+
101
+ module.exports = { createMatterhornOperationEvent, createMatterhornOperationEventBundle };
@@ -0,0 +1,39 @@
1
+ export interface MatterhornOperationEventOptions {
2
+ identity: unknown;
3
+ operation: {
4
+ id?: string;
5
+ opId?: string;
6
+ hlc?: unknown;
7
+ pluginId?: string;
8
+ type?: string;
9
+ schemaAction?: string;
10
+ actor?: {
11
+ memberId?: string;
12
+ deviceId?: string;
13
+ role?: string;
14
+ };
15
+ grants?: unknown;
16
+ epoch?: unknown;
17
+ stream?: unknown;
18
+ seq?: unknown;
19
+ appPackId?: string;
20
+ appPackHash?: string;
21
+ payload?: unknown;
22
+ };
23
+ roomName: string;
24
+ roomSecret: string;
25
+ createdAt?: number;
26
+ }
27
+
28
+ export interface MatterhornOperationEventBundleOptions extends MatterhornOperationEventOptions {
29
+ roomIndexKey?: string;
30
+ indexText?: unknown | unknown[];
31
+ indexPaths?: string[];
32
+ indexField?: string;
33
+ minGram?: number;
34
+ maxGram?: number;
35
+ maxTokensPerEvent?: number;
36
+ }
37
+
38
+ export function createMatterhornOperationEvent(options: MatterhornOperationEventOptions): unknown;
39
+ export function createMatterhornOperationEventBundle(options: MatterhornOperationEventBundleOptions): { event: unknown; indexEvents: unknown[]; events: unknown[] };
@@ -0,0 +1,9 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const {
5
+ createMatterhornOperationEvent,
6
+ createMatterhornOperationEventBundle
7
+ } = require("./matterhornOperationBuilder.cjs");
8
+
9
+ export { createMatterhornOperationEvent, createMatterhornOperationEventBundle };