@tinyhumansai/tinyplace 0.1.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.
Files changed (170) hide show
  1. package/dist/api/a2a.d.ts +28 -0
  2. package/dist/api/a2a.js +21 -0
  3. package/dist/api/admin.d.ts +38 -0
  4. package/dist/api/admin.js +49 -0
  5. package/dist/api/broadcasts.d.ts +32 -0
  6. package/dist/api/broadcasts.js +51 -0
  7. package/dist/api/channels.d.ts +43 -0
  8. package/dist/api/channels.js +57 -0
  9. package/dist/api/directory.d.ts +15 -0
  10. package/dist/api/directory.js +26 -0
  11. package/dist/api/escrow.d.ts +47 -0
  12. package/dist/api/escrow.js +76 -0
  13. package/dist/api/events.d.ts +45 -0
  14. package/dist/api/events.js +77 -0
  15. package/dist/api/explorer.d.ts +19 -0
  16. package/dist/api/explorer.js +21 -0
  17. package/dist/api/groups.d.ts +19 -0
  18. package/dist/api/groups.js +32 -0
  19. package/dist/api/inbox.d.ts +27 -0
  20. package/dist/api/inbox.js +48 -0
  21. package/dist/api/keys.d.ts +9 -0
  22. package/dist/api/keys.js +14 -0
  23. package/dist/api/ledger.d.ts +11 -0
  24. package/dist/api/ledger.js +14 -0
  25. package/dist/api/marketplace.d.ts +53 -0
  26. package/dist/api/marketplace.js +81 -0
  27. package/dist/api/messages.d.ts +11 -0
  28. package/dist/api/messages.js +17 -0
  29. package/dist/api/moderation.d.ts +30 -0
  30. package/dist/api/moderation.js +32 -0
  31. package/dist/api/payments.d.ts +15 -0
  32. package/dist/api/payments.js +26 -0
  33. package/dist/api/pricing.d.ts +69 -0
  34. package/dist/api/pricing.js +60 -0
  35. package/dist/api/profiles.d.ts +18 -0
  36. package/dist/api/profiles.js +23 -0
  37. package/dist/api/registry.d.ts +26 -0
  38. package/dist/api/registry.js +87 -0
  39. package/dist/api/reputation.d.ts +24 -0
  40. package/dist/api/reputation.js +29 -0
  41. package/dist/api/search.d.ts +46 -0
  42. package/dist/api/search.js +41 -0
  43. package/dist/api/stats.d.ts +11 -0
  44. package/dist/api/stats.js +20 -0
  45. package/dist/auth.d.ts +16 -0
  46. package/dist/auth.js +36 -0
  47. package/dist/client.d.ts +63 -0
  48. package/dist/client.js +73 -0
  49. package/dist/crypto.d.ts +12 -0
  50. package/dist/crypto.js +49 -0
  51. package/dist/http.d.ts +30 -0
  52. package/dist/http.js +101 -0
  53. package/dist/index.d.ts +39 -0
  54. package/dist/index.js +32 -0
  55. package/dist/local-signer.d.ts +15 -0
  56. package/dist/local-signer.js +51 -0
  57. package/dist/signal/crypto.d.ts +29 -0
  58. package/dist/signal/crypto.js +156 -0
  59. package/dist/signal/index.d.ts +11 -0
  60. package/dist/signal/index.js +6 -0
  61. package/dist/signal/keys.d.ts +14 -0
  62. package/dist/signal/keys.js +36 -0
  63. package/dist/signal/memory-store.d.ts +21 -0
  64. package/dist/signal/memory-store.js +50 -0
  65. package/dist/signal/ratchet.d.ts +12 -0
  66. package/dist/signal/ratchet.js +106 -0
  67. package/dist/signal/session.d.ts +17 -0
  68. package/dist/signal/session.js +117 -0
  69. package/dist/signal/store.d.ts +36 -0
  70. package/dist/signal/store.js +6 -0
  71. package/dist/signal/x3dh.d.ts +18 -0
  72. package/dist/signal/x3dh.js +86 -0
  73. package/dist/signer.d.ts +13 -0
  74. package/dist/signer.js +9 -0
  75. package/dist/types/broadcasts.d.ts +74 -0
  76. package/dist/types/broadcasts.js +1 -0
  77. package/dist/types/commerce.d.ts +183 -0
  78. package/dist/types/commerce.js +1 -0
  79. package/dist/types/directory.d.ts +88 -0
  80. package/dist/types/directory.js +1 -0
  81. package/dist/types/escrow.d.ts +129 -0
  82. package/dist/types/escrow.js +1 -0
  83. package/dist/types/events.d.ts +137 -0
  84. package/dist/types/events.js +1 -0
  85. package/dist/types/explorer.d.ts +133 -0
  86. package/dist/types/explorer.js +1 -0
  87. package/dist/types/groups.d.ts +56 -0
  88. package/dist/types/groups.js +1 -0
  89. package/dist/types/identity.d.ts +94 -0
  90. package/dist/types/identity.js +1 -0
  91. package/dist/types/index.d.ts +16 -0
  92. package/dist/types/index.js +16 -0
  93. package/dist/types/ledger.d.ts +57 -0
  94. package/dist/types/ledger.js +1 -0
  95. package/dist/types/marketplace.d.ts +141 -0
  96. package/dist/types/marketplace.js +1 -0
  97. package/dist/types/messaging.d.ts +67 -0
  98. package/dist/types/messaging.js +1 -0
  99. package/dist/types/payments.d.ts +88 -0
  100. package/dist/types/payments.js +1 -0
  101. package/dist/types/profile.d.ts +49 -0
  102. package/dist/types/profile.js +1 -0
  103. package/dist/types/reputation.d.ts +90 -0
  104. package/dist/types/reputation.js +1 -0
  105. package/dist/types/search.d.ts +56 -0
  106. package/dist/types/search.js +1 -0
  107. package/dist/types/social.d.ts +158 -0
  108. package/dist/types/social.js +1 -0
  109. package/dist/websocket.d.ts +26 -0
  110. package/dist/websocket.js +83 -0
  111. package/package.json +30 -0
  112. package/src/api/a2a.ts +50 -0
  113. package/src/api/admin.ts +95 -0
  114. package/src/api/broadcasts.ts +110 -0
  115. package/src/api/channels.ts +110 -0
  116. package/src/api/directory.ts +45 -0
  117. package/src/api/escrow.ts +163 -0
  118. package/src/api/events.ts +133 -0
  119. package/src/api/explorer.ts +48 -0
  120. package/src/api/groups.ts +64 -0
  121. package/src/api/inbox.ts +71 -0
  122. package/src/api/keys.ts +18 -0
  123. package/src/api/ledger.ts +28 -0
  124. package/src/api/marketplace.ts +165 -0
  125. package/src/api/messages.ts +23 -0
  126. package/src/api/moderation.ts +71 -0
  127. package/src/api/payments.ts +47 -0
  128. package/src/api/pricing.ts +122 -0
  129. package/src/api/profiles.ts +43 -0
  130. package/src/api/registry.ts +143 -0
  131. package/src/api/reputation.ts +60 -0
  132. package/src/api/search.ts +59 -0
  133. package/src/api/stats.ts +32 -0
  134. package/src/auth.ts +75 -0
  135. package/src/client.ts +120 -0
  136. package/src/crypto.ts +74 -0
  137. package/src/http.ts +147 -0
  138. package/src/index.ts +72 -0
  139. package/src/local-signer.ts +78 -0
  140. package/src/signal/crypto.ts +229 -0
  141. package/src/signal/index.ts +28 -0
  142. package/src/signal/keys.ts +54 -0
  143. package/src/signal/memory-store.ts +66 -0
  144. package/src/signal/ratchet.ts +162 -0
  145. package/src/signal/session.ts +189 -0
  146. package/src/signal/store.ts +49 -0
  147. package/src/signal/x3dh.ts +130 -0
  148. package/src/signer.ts +21 -0
  149. package/src/types/broadcasts.ts +81 -0
  150. package/src/types/commerce.ts +206 -0
  151. package/src/types/directory.ts +98 -0
  152. package/src/types/escrow.ts +163 -0
  153. package/src/types/events.ts +155 -0
  154. package/src/types/explorer.ts +152 -0
  155. package/src/types/groups.ts +62 -0
  156. package/src/types/identity.ts +113 -0
  157. package/src/types/index.ts +16 -0
  158. package/src/types/ledger.ts +78 -0
  159. package/src/types/marketplace.ts +166 -0
  160. package/src/types/messaging.ts +77 -0
  161. package/src/types/payments.ts +103 -0
  162. package/src/types/profile.ts +55 -0
  163. package/src/types/reputation.ts +98 -0
  164. package/src/types/search.ts +61 -0
  165. package/src/types/social.ts +186 -0
  166. package/src/websocket.ts +112 -0
  167. package/tests/signal.test.ts +353 -0
  168. package/tests/staging.test.ts +650 -0
  169. package/tsconfig.json +15 -0
  170. package/vitest.config.ts +7 -0
package/src/crypto.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
+ import type { SigningKey } from "./auth.js";
3
+
4
+ const crypto = globalThis.crypto;
5
+
6
+ export interface KeyPair {
7
+ publicKey: Uint8Array;
8
+ privateKey: CryptoKey;
9
+ }
10
+
11
+ export async function generateKeyPair(): Promise<KeyPair> {
12
+ const pair = await crypto.subtle.generateKey("Ed25519", true, [
13
+ "sign",
14
+ "verify",
15
+ ]);
16
+ const publicKeyRaw = new Uint8Array(
17
+ await crypto.subtle.exportKey("raw", pair.publicKey),
18
+ );
19
+ return { publicKey: publicKeyRaw, privateKey: pair.privateKey };
20
+ }
21
+
22
+ function toHex(bytes: Uint8Array): string {
23
+ return Array.from(bytes)
24
+ .map((b) => b.toString(16).padStart(2, "0"))
25
+ .join("");
26
+ }
27
+
28
+ function toBase64(bytes: Uint8Array): string {
29
+ let binary = "";
30
+ for (const byte of bytes) {
31
+ binary += String.fromCharCode(byte);
32
+ }
33
+ return btoa(binary);
34
+ }
35
+
36
+ export function publicKeyToHex(publicKey: Uint8Array): string {
37
+ return toHex(publicKey);
38
+ }
39
+
40
+ export function publicKeyToBase64(publicKey: Uint8Array): string {
41
+ return toBase64(publicKey);
42
+ }
43
+
44
+ export function deriveCryptoId(publicKey: Uint8Array): string {
45
+ return `tiny1${toHex(publicKey).slice(0, 40)}`;
46
+ }
47
+
48
+ export function sha256Hex(data: Uint8Array | string): string {
49
+ const input =
50
+ typeof data === "string" ? new TextEncoder().encode(data) : data;
51
+ return toHex(sha256(input));
52
+ }
53
+
54
+ export function canonicalPayload(
55
+ action: string,
56
+ fields: Record<string, unknown>,
57
+ ): string {
58
+ return JSON.stringify({ action, fields });
59
+ }
60
+
61
+ export function createSigningKey(
62
+ agentId: string,
63
+ privateKey: CryptoKey,
64
+ ): SigningKey {
65
+ return {
66
+ agentId,
67
+ async sign(data: Uint8Array): Promise<Uint8Array> {
68
+ const buffer = new ArrayBuffer(data.byteLength);
69
+ new Uint8Array(buffer).set(data);
70
+ const sig = await crypto.subtle.sign("Ed25519", privateKey, buffer);
71
+ return new Uint8Array(sig);
72
+ },
73
+ };
74
+ }
package/src/http.ts ADDED
@@ -0,0 +1,147 @@
1
+ import type { SigningKey } from "./auth.js";
2
+ import { signRequest, signDirectoryWrite } from "./auth.js";
3
+
4
+ export class TinyVerseError extends Error {
5
+ constructor(
6
+ public readonly status: number,
7
+ public readonly body: unknown,
8
+ message?: string,
9
+ ) {
10
+ super(message ?? `HTTP ${status}`);
11
+ this.name = "TinyVerseError";
12
+ }
13
+ }
14
+
15
+ export interface HttpClientOptions {
16
+ baseUrl: string;
17
+ signingKey?: SigningKey;
18
+ publicKeyBase64?: string;
19
+ fetch?: typeof globalThis.fetch;
20
+ }
21
+
22
+ function buildQuery(params: Record<string, unknown>): string {
23
+ const parts: Array<string> = [];
24
+ for (const [key, value] of Object.entries(params)) {
25
+ if (value === undefined || value === null) continue;
26
+ if (Array.isArray(value)) {
27
+ for (const item of value) {
28
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
29
+ }
30
+ } else {
31
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
32
+ }
33
+ }
34
+ return parts.length > 0 ? `?${parts.join("&")}` : "";
35
+ }
36
+
37
+ export class HttpClient {
38
+ private readonly baseUrl: string;
39
+ private readonly signingKey?: SigningKey;
40
+ private readonly publicKeyBase64?: string;
41
+ private readonly _fetch: typeof globalThis.fetch;
42
+
43
+ constructor(options: HttpClientOptions) {
44
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
45
+ this.signingKey = options.signingKey;
46
+ this.publicKeyBase64 = options.publicKeyBase64;
47
+ this._fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
48
+ }
49
+
50
+ private async request<T>(
51
+ method: string,
52
+ path: string,
53
+ options?: {
54
+ body?: unknown;
55
+ query?: Record<string, unknown>;
56
+ signed?: boolean;
57
+ directoryAuth?: boolean;
58
+ },
59
+ ): Promise<T> {
60
+ const queryString = options?.query ? buildQuery(options.query) : "";
61
+ const url = `${this.baseUrl}${path}${queryString}`;
62
+ const headers: Record<string, string> = {
63
+ "Content-Type": "application/json",
64
+ };
65
+
66
+ const bodyStr = options?.body != null ? JSON.stringify(options.body) : "";
67
+
68
+ if (options?.directoryAuth && this.signingKey && this.publicKeyBase64) {
69
+ const requestUri = `${path}${queryString}`;
70
+ const writeHeaders = await signDirectoryWrite(
71
+ this.signingKey,
72
+ this.publicKeyBase64,
73
+ method,
74
+ requestUri,
75
+ bodyStr,
76
+ );
77
+ Object.assign(headers, writeHeaders);
78
+ headers["X-Agent-ID"] = this.publicKeyBase64;
79
+ } else if (options?.signed && this.signingKey) {
80
+ const authHeaders = await signRequest(this.signingKey, bodyStr);
81
+ Object.assign(headers, authHeaders);
82
+ }
83
+
84
+ const response = await this._fetch(url, {
85
+ method,
86
+ headers,
87
+ body: bodyStr || undefined,
88
+ });
89
+
90
+ if (!response.ok) {
91
+ const errorBody = await response.text().catch(() => "");
92
+ let parsed: unknown;
93
+ try {
94
+ parsed = JSON.parse(errorBody);
95
+ } catch {
96
+ parsed = errorBody;
97
+ }
98
+ throw new TinyVerseError(response.status, parsed, `HTTP ${response.status}: ${path}`);
99
+ }
100
+
101
+ if (response.status === 204) {
102
+ return undefined as T;
103
+ }
104
+
105
+ return response.json() as Promise<T>;
106
+ }
107
+
108
+ get<T>(path: string, query?: Record<string, unknown>): Promise<T> {
109
+ return this.request<T>("GET", path, { query });
110
+ }
111
+
112
+ getAuth<T>(path: string, query?: Record<string, unknown>): Promise<T> {
113
+ return this.request<T>("GET", path, { query, signed: true });
114
+ }
115
+
116
+ getDirectoryAuth<T>(path: string, query?: Record<string, unknown>): Promise<T> {
117
+ return this.request<T>("GET", path, { query, directoryAuth: true });
118
+ }
119
+
120
+ post<T>(path: string, body?: unknown): Promise<T> {
121
+ return this.request<T>("POST", path, { body, signed: true });
122
+ }
123
+
124
+ postPublic<T>(path: string, body?: unknown): Promise<T> {
125
+ return this.request<T>("POST", path, { body });
126
+ }
127
+
128
+ put<T>(path: string, body?: unknown): Promise<T> {
129
+ return this.request<T>("PUT", path, { body, signed: true });
130
+ }
131
+
132
+ postDirectoryAuth<T>(path: string, body?: unknown): Promise<T> {
133
+ return this.request<T>("POST", path, { body, directoryAuth: true });
134
+ }
135
+
136
+ putDirectoryAuth<T>(path: string, body?: unknown): Promise<T> {
137
+ return this.request<T>("PUT", path, { body, directoryAuth: true });
138
+ }
139
+
140
+ delete<T>(path: string, body?: unknown): Promise<T> {
141
+ return this.request<T>("DELETE", path, { body, signed: true });
142
+ }
143
+
144
+ deleteDirectoryAuth<T>(path: string, body?: unknown): Promise<T> {
145
+ return this.request<T>("DELETE", path, { body, directoryAuth: true });
146
+ }
147
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ export const SDK_VERSION = "0.1.0";
2
+
3
+ export { TinyVerseClient } from "./client.js";
4
+ export type { TinyVerseClientOptions } from "./client.js";
5
+
6
+ export { TinyVerseError } from "./http.js";
7
+ export { TinyVerseWebSocket } from "./websocket.js";
8
+ export type { TinyVerseWebSocketOptions, WebSocketEventHandler } from "./websocket.js";
9
+
10
+ export type { SigningKey, AuthHeaders, DirectoryWriteHeaders } from "./auth.js";
11
+ export { buildAuthHeader, signRequest, signDirectoryWrite, signCanonicalPayload } from "./auth.js";
12
+
13
+ export { Signer } from "./signer.js";
14
+ export { LocalSigner } from "./local-signer.js";
15
+
16
+ export type { KeyPair } from "./crypto.js";
17
+ export {
18
+ generateKeyPair,
19
+ publicKeyToHex,
20
+ publicKeyToBase64,
21
+ deriveCryptoId,
22
+ sha256Hex,
23
+ canonicalPayload,
24
+ createSigningKey,
25
+ } from "./crypto.js";
26
+
27
+ export type { RegisterRequest } from "./api/registry.js";
28
+
29
+ export { RegistryApi } from "./api/registry.js";
30
+ export { KeysApi } from "./api/keys.js";
31
+ export { MessagesApi } from "./api/messages.js";
32
+ export { DirectoryApi } from "./api/directory.js";
33
+ export { GroupsApi } from "./api/groups.js";
34
+ export { PaymentsApi } from "./api/payments.js";
35
+ export { LedgerApi } from "./api/ledger.js";
36
+ export { ReputationApi } from "./api/reputation.js";
37
+ export { InboxApi } from "./api/inbox.js";
38
+ export { ChannelsApi } from "./api/channels.js";
39
+ export { BroadcastsApi } from "./api/broadcasts.js";
40
+ export { EventsApi } from "./api/events.js";
41
+ export { MarketplaceApi } from "./api/marketplace.js";
42
+ export { EscrowApi } from "./api/escrow.js";
43
+ export { SearchApi } from "./api/search.js";
44
+ export { ProfilesApi } from "./api/profiles.js";
45
+ export { ExplorerApi } from "./api/explorer.js";
46
+ export { PricingApi } from "./api/pricing.js";
47
+ export { ModerationApi } from "./api/moderation.js";
48
+ export { StatsApi } from "./api/stats.js";
49
+ export { AdminApi } from "./api/admin.js";
50
+ export { A2AApi } from "./api/a2a.js";
51
+ export type { A2ATaskRequest, A2ATaskResponse } from "./api/a2a.js";
52
+
53
+ export * from "./types/index.js";
54
+
55
+ export {
56
+ SignalSession,
57
+ MemorySessionStore,
58
+ generateSignedPreKey,
59
+ generatePreKeys,
60
+ serializeSignedKey,
61
+ serializePreKey,
62
+ generateX25519KeyPair,
63
+ ed25519PubToX25519Pub,
64
+ } from "./signal/index.js";
65
+ export type {
66
+ SessionStore,
67
+ SessionState,
68
+ PreKeyPair,
69
+ SignedPreKeyPair,
70
+ X25519KeyPair,
71
+ EncryptedMessage,
72
+ } from "./signal/index.js";
@@ -0,0 +1,78 @@
1
+ import { Signer } from "./signer.js";
2
+ import {
3
+ generateKeyPair,
4
+ deriveCryptoId,
5
+ publicKeyToBase64,
6
+ } from "./crypto.js";
7
+ import type { KeyPair } from "./crypto.js";
8
+ import { ed25519SeedToX25519KeyPair } from "./signal/crypto.js";
9
+ import type { X25519KeyPair } from "./signal/crypto.js";
10
+
11
+ export class LocalSigner extends Signer {
12
+ readonly agentId: string;
13
+ readonly publicKeyBase64: string;
14
+ readonly publicKey: Uint8Array;
15
+
16
+ private readonly privateKey: CryptoKey;
17
+
18
+ private constructor(keyPair: KeyPair) {
19
+ super();
20
+ this.publicKey = keyPair.publicKey;
21
+ this.privateKey = keyPair.privateKey;
22
+ this.agentId = deriveCryptoId(keyPair.publicKey);
23
+ this.publicKeyBase64 = publicKeyToBase64(keyPair.publicKey);
24
+ }
25
+
26
+ static async generate(): Promise<LocalSigner> {
27
+ const keyPair = await generateKeyPair();
28
+ return new LocalSigner(keyPair);
29
+ }
30
+
31
+ static async fromPrivateKey(privateKey: CryptoKey): Promise<LocalSigner> {
32
+ const crypto = globalThis.crypto;
33
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey);
34
+ const publicOnlyJwk = { ...jwk, d: undefined, key_ops: ["verify"] };
35
+ const publicCryptoKey = await crypto.subtle.importKey(
36
+ "jwk",
37
+ publicOnlyJwk,
38
+ { name: "Ed25519" },
39
+ true,
40
+ ["verify"],
41
+ );
42
+ const publicKeyRaw = new Uint8Array(
43
+ await crypto.subtle.exportKey("raw", publicCryptoKey),
44
+ );
45
+ return new LocalSigner({ publicKey: publicKeyRaw, privateKey });
46
+ }
47
+
48
+ static fromKeyPair(keyPair: KeyPair): LocalSigner {
49
+ return new LocalSigner(keyPair);
50
+ }
51
+
52
+ async sign(data: Uint8Array): Promise<Uint8Array> {
53
+ const crypto = globalThis.crypto;
54
+ const buffer = new ArrayBuffer(data.byteLength);
55
+ new Uint8Array(buffer).set(data);
56
+ const sig = await crypto.subtle.sign("Ed25519", this.privateKey, buffer);
57
+ return new Uint8Array(sig);
58
+ }
59
+
60
+ async getX25519KeyPair(): Promise<X25519KeyPair> {
61
+ const crypto = globalThis.crypto;
62
+ const jwk = await crypto.subtle.exportKey("jwk", this.privateKey);
63
+ const seed = base64urlToBytes(jwk.d!);
64
+ return ed25519SeedToX25519KeyPair(seed);
65
+ }
66
+ }
67
+
68
+ function base64urlToBytes(b64url: string): Uint8Array {
69
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
70
+ const pad = (4 - (b64.length % 4)) % 4;
71
+ const padded = b64 + "=".repeat(pad);
72
+ const binary = atob(padded);
73
+ const bytes = new Uint8Array(binary.length);
74
+ for (let i = 0; i < binary.length; i++) {
75
+ bytes[i] = binary.charCodeAt(i);
76
+ }
77
+ return bytes;
78
+ }
@@ -0,0 +1,229 @@
1
+ import { x25519 } from "@noble/curves/ed25519.js";
2
+ import { hkdf } from "@noble/hashes/hkdf.js";
3
+ import { sha256, sha512 } from "@noble/hashes/sha2.js";
4
+ import { hmac } from "@noble/hashes/hmac.js";
5
+ const crypto = globalThis.crypto;
6
+
7
+ const HKDF_INFO = new TextEncoder().encode("WhisperRatchet");
8
+ const MESSAGE_KEY_INFO = new TextEncoder().encode("WhisperMessageKeys");
9
+ const CHAIN_KEY_SEED_MESSAGE = new Uint8Array([0x01]);
10
+ const CHAIN_KEY_SEED_CHAIN = new Uint8Array([0x02]);
11
+
12
+ export interface X25519KeyPair {
13
+ publicKey: Uint8Array;
14
+ privateKey: Uint8Array;
15
+ }
16
+
17
+ export function generateX25519KeyPair(): X25519KeyPair {
18
+ const privateKey = x25519.utils.randomSecretKey();
19
+ const publicKey = x25519.getPublicKey(privateKey);
20
+ return { publicKey, privateKey };
21
+ }
22
+
23
+ export function x25519SharedSecret(
24
+ privateKey: Uint8Array,
25
+ publicKey: Uint8Array,
26
+ ): Uint8Array {
27
+ return x25519.getSharedSecret(privateKey, publicKey);
28
+ }
29
+
30
+ export function ed25519SeedToX25519Private(seed: Uint8Array): Uint8Array {
31
+ const hash = sha512(seed);
32
+ const scalar = hash.slice(0, 32);
33
+ scalar[0]! &= 248;
34
+ scalar[31]! &= 127;
35
+ scalar[31]! |= 64;
36
+ return scalar;
37
+ }
38
+
39
+ export function ed25519SeedToX25519KeyPair(seed: Uint8Array): X25519KeyPair {
40
+ const privateKey = ed25519SeedToX25519Private(seed);
41
+ const publicKey = x25519.getPublicKey(privateKey);
42
+ return { publicKey, privateKey };
43
+ }
44
+
45
+ // Edwards (Ed25519) public key → Montgomery (X25519) public key.
46
+ // Formula: u = (1 + y) / (1 - y) mod p, where p = 2^255 - 19.
47
+ const P = (1n << 255n) - 19n;
48
+
49
+ function modPow(base: bigint, exp: bigint, modulus: bigint): bigint {
50
+ let result = 1n;
51
+ base = ((base % modulus) + modulus) % modulus;
52
+ while (exp > 0n) {
53
+ if (exp & 1n) result = (result * base) % modulus;
54
+ exp >>= 1n;
55
+ base = (base * base) % modulus;
56
+ }
57
+ return result;
58
+ }
59
+
60
+ function modInverse(a: bigint, modulus: bigint): bigint {
61
+ return modPow(a, modulus - 2n, modulus);
62
+ }
63
+
64
+ export function ed25519PubToX25519Pub(edPub: Uint8Array): Uint8Array {
65
+ // Ed25519 public key encodes the y-coordinate in little-endian with
66
+ // the sign bit in the top bit of the last byte.
67
+ const bytes = new Uint8Array(edPub);
68
+ bytes[31] = bytes[31]! & 0x7f;
69
+ let y = 0n;
70
+ for (let i = 0; i < 32; i++) {
71
+ y |= BigInt(bytes[i]!) << BigInt(8 * i);
72
+ }
73
+ const numerator = ((1n + y) % P + P) % P;
74
+ const denominator = ((1n - y) % P + P) % P;
75
+ const u = (numerator * modInverse(denominator, P)) % P;
76
+ const result = new Uint8Array(32);
77
+ let val = u;
78
+ for (let i = 0; i < 32; i++) {
79
+ result[i] = Number(val & 0xffn);
80
+ val >>= 8n;
81
+ }
82
+ return result;
83
+ }
84
+
85
+ export function kdfRootKey(
86
+ rootKey: Uint8Array,
87
+ dhOutput: Uint8Array,
88
+ ): { rootKey: Uint8Array; chainKey: Uint8Array } {
89
+ const output = hkdf(sha256, dhOutput, rootKey, HKDF_INFO, 64);
90
+ return {
91
+ rootKey: output.slice(0, 32),
92
+ chainKey: output.slice(32, 64),
93
+ };
94
+ }
95
+
96
+ export function kdfChainKey(chainKey: Uint8Array): {
97
+ chainKey: Uint8Array;
98
+ messageKey: Uint8Array;
99
+ } {
100
+ const newChainKey = hmac(sha256, chainKey, CHAIN_KEY_SEED_CHAIN);
101
+ const messageKey = hmac(sha256, chainKey, CHAIN_KEY_SEED_MESSAGE);
102
+ return { chainKey: newChainKey, messageKey };
103
+ }
104
+
105
+ export function deriveMessageKeys(messageKey: Uint8Array): {
106
+ encKey: Uint8Array;
107
+ macKey: Uint8Array;
108
+ iv: Uint8Array;
109
+ } {
110
+ const output = hkdf(sha256, messageKey, new Uint8Array(32), MESSAGE_KEY_INFO, 80);
111
+ return {
112
+ encKey: output.slice(0, 32),
113
+ macKey: output.slice(32, 64),
114
+ iv: output.slice(64, 80),
115
+ };
116
+ }
117
+
118
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
119
+ const buffer = new ArrayBuffer(bytes.length);
120
+ new Uint8Array(buffer).set(bytes);
121
+ return buffer;
122
+ }
123
+
124
+ export async function aesEncrypt(
125
+ key: Uint8Array,
126
+ iv: Uint8Array,
127
+ plaintext: Uint8Array,
128
+ ): Promise<Uint8Array> {
129
+ const crypto = globalThis.crypto;
130
+ const cryptoKey = await crypto.subtle.importKey(
131
+ "raw",
132
+ toArrayBuffer(key),
133
+ { name: "AES-CBC" },
134
+ false,
135
+ ["encrypt"],
136
+ );
137
+ const ciphertext = await crypto.subtle.encrypt(
138
+ { name: "AES-CBC", iv: toArrayBuffer(iv) },
139
+ cryptoKey,
140
+ toArrayBuffer(plaintext),
141
+ );
142
+ return new Uint8Array(ciphertext);
143
+ }
144
+
145
+ export async function aesDecrypt(
146
+ key: Uint8Array,
147
+ iv: Uint8Array,
148
+ ciphertext: Uint8Array,
149
+ ): Promise<Uint8Array> {
150
+ const crypto = globalThis.crypto;
151
+ const cryptoKey = await crypto.subtle.importKey(
152
+ "raw",
153
+ toArrayBuffer(key),
154
+ { name: "AES-CBC" },
155
+ false,
156
+ ["decrypt"],
157
+ );
158
+ const plaintext = await crypto.subtle.decrypt(
159
+ { name: "AES-CBC", iv: toArrayBuffer(iv) },
160
+ cryptoKey,
161
+ toArrayBuffer(ciphertext),
162
+ );
163
+ return new Uint8Array(plaintext);
164
+ }
165
+
166
+ export function computeHmac(key: Uint8Array, data: Uint8Array): Uint8Array {
167
+ return hmac(sha256, key, data);
168
+ }
169
+
170
+ export async function encrypt(
171
+ messageKey: Uint8Array,
172
+ plaintext: Uint8Array,
173
+ associatedData: Uint8Array,
174
+ ): Promise<Uint8Array> {
175
+ const { encKey, macKey, iv } = deriveMessageKeys(messageKey);
176
+ const ciphertext = await aesEncrypt(encKey, iv, plaintext);
177
+ const macInput = new Uint8Array(associatedData.length + ciphertext.length);
178
+ macInput.set(associatedData);
179
+ macInput.set(ciphertext, associatedData.length);
180
+ const mac = computeHmac(macKey, macInput).slice(0, 8);
181
+ const result = new Uint8Array(ciphertext.length + 8);
182
+ result.set(ciphertext);
183
+ result.set(mac, ciphertext.length);
184
+ return result;
185
+ }
186
+
187
+ export async function decrypt(
188
+ messageKey: Uint8Array,
189
+ ciphertextWithMac: Uint8Array,
190
+ associatedData: Uint8Array,
191
+ ): Promise<Uint8Array> {
192
+ const { encKey, macKey, iv } = deriveMessageKeys(messageKey);
193
+ const ciphertext = ciphertextWithMac.slice(0, -8);
194
+ const receivedMac = ciphertextWithMac.slice(-8);
195
+ const macInput = new Uint8Array(associatedData.length + ciphertext.length);
196
+ macInput.set(associatedData);
197
+ macInput.set(ciphertext, associatedData.length);
198
+ const computedMac = computeHmac(macKey, macInput).slice(0, 8);
199
+ if (!constantTimeEqual(receivedMac, computedMac)) {
200
+ throw new Error("MAC verification failed");
201
+ }
202
+ return aesDecrypt(encKey, iv, ciphertext);
203
+ }
204
+
205
+ function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
206
+ if (a.length !== b.length) return false;
207
+ let diff = 0;
208
+ for (let i = 0; i < a.length; i++) {
209
+ diff |= a[i]! ^ b[i]!;
210
+ }
211
+ return diff === 0;
212
+ }
213
+
214
+ export function toBase64(bytes: Uint8Array): string {
215
+ let binary = "";
216
+ for (const byte of bytes) {
217
+ binary += String.fromCharCode(byte);
218
+ }
219
+ return btoa(binary);
220
+ }
221
+
222
+ export function fromBase64(b64: string): Uint8Array {
223
+ const binary = atob(b64);
224
+ const bytes = new Uint8Array(binary.length);
225
+ for (let i = 0; i < binary.length; i++) {
226
+ bytes[i] = binary.charCodeAt(i);
227
+ }
228
+ return bytes;
229
+ }
@@ -0,0 +1,28 @@
1
+ export type { X25519KeyPair } from "./crypto.js";
2
+ export {
3
+ generateX25519KeyPair,
4
+ ed25519PubToX25519Pub,
5
+ ed25519SeedToX25519KeyPair,
6
+ toBase64,
7
+ fromBase64,
8
+ } from "./crypto.js";
9
+
10
+ export type { SessionState, PreKeyPair, SignedPreKeyPair, SessionStore } from "./store.js";
11
+
12
+ export { MemorySessionStore } from "./memory-store.js";
13
+
14
+ export type { X3DHBundle, X3DHInitResult } from "./x3dh.js";
15
+ export { x3dhInitiate, x3dhRespond, buildAssociatedData } from "./x3dh.js";
16
+
17
+ export type { RatchetHeader, RatchetMessage } from "./ratchet.js";
18
+ export { ratchetEncrypt, ratchetDecrypt } from "./ratchet.js";
19
+
20
+ export {
21
+ generateSignedPreKey,
22
+ generatePreKeys,
23
+ serializeSignedKey,
24
+ serializePreKey,
25
+ } from "./keys.js";
26
+
27
+ export type { EncryptedMessage } from "./session.js";
28
+ export { SignalSession } from "./session.js";
@@ -0,0 +1,54 @@
1
+ import { generateX25519KeyPair, toBase64 } from "./crypto.js";
2
+ import type { Signer } from "../signer.js";
3
+ import type { PreKeyPair, SignedPreKeyPair } from "./store.js";
4
+
5
+ // Backend verifies: ed25519.Verify(identityPubKey, []byte(preKey.PublicKey), signature)
6
+ // So we sign the base64 string representation of the X25519 public key.
7
+ async function signPublicKey(signer: Signer, publicKey: Uint8Array): Promise<Uint8Array> {
8
+ const publicKeyB64 = toBase64(publicKey);
9
+ return signer.sign(new TextEncoder().encode(publicKeyB64));
10
+ }
11
+
12
+ export async function generateSignedPreKey(
13
+ signer: Signer,
14
+ keyId: string,
15
+ ): Promise<SignedPreKeyPair> {
16
+ const keyPair = generateX25519KeyPair();
17
+ const signature = await signPublicKey(signer, keyPair.publicKey);
18
+ return { keyId, keyPair, signature };
19
+ }
20
+
21
+ export async function generatePreKeys(
22
+ signer: Signer,
23
+ startId: number,
24
+ count: number,
25
+ ): Promise<Array<PreKeyPair>> {
26
+ const preKeys: Array<PreKeyPair> = [];
27
+ for (let i = 0; i < count; i++) {
28
+ const keyId = `pk_${startId + i}`;
29
+ const keyPair = generateX25519KeyPair();
30
+ const signature = await signPublicKey(signer, keyPair.publicKey);
31
+ preKeys.push({ keyId, keyPair, signature });
32
+ }
33
+ return preKeys;
34
+ }
35
+
36
+ export function serializeSignedKey(
37
+ preKey: SignedPreKeyPair,
38
+ ): { keyId: string; publicKey: string; signature: string } {
39
+ return {
40
+ keyId: preKey.keyId,
41
+ publicKey: toBase64(preKey.keyPair.publicKey),
42
+ signature: toBase64(preKey.signature),
43
+ };
44
+ }
45
+
46
+ export function serializePreKey(
47
+ preKey: PreKeyPair,
48
+ ): { keyId: string; publicKey: string; signature: string } {
49
+ return {
50
+ keyId: preKey.keyId,
51
+ publicKey: toBase64(preKey.keyPair.publicKey),
52
+ signature: toBase64(preKey.signature),
53
+ };
54
+ }