@genation/bridge-peer 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.
- package/GenationBridgePeer.podspec +17 -0
- package/LICENSE +190 -0
- package/README.md +66 -0
- package/package.json +59 -0
- package/react-native/android/build.gradle +24 -0
- package/react-native/android/src/main/java/com/genation/bridge/GenationBridgePeerModule.kt +182 -0
- package/react-native/android/src/main/java/com/genation/bridge/GenationBridgePeerPackage.kt +15 -0
- package/react-native/android/src/main/jniLibs/arm64-v8a/libgenation_bridge_crypto_uniffi.so +0 -0
- package/react-native/android/src/main/jniLibs/armeabi-v7a/libgenation_bridge_crypto_uniffi.so +0 -0
- package/react-native/android/src/main/jniLibs/x86/libgenation_bridge_crypto_uniffi.so +0 -0
- package/react-native/android/src/main/jniLibs/x86_64/libgenation_bridge_crypto_uniffi.so +0 -0
- package/react-native/generated/kotlin/uniffi/genation_bridge_crypto_uniffi/genation_bridge_crypto_uniffi.kt +2006 -0
- package/react-native/generated/swift/GenationBridgeCryptoMobile.swift +1072 -0
- package/react-native/generated/swift/GenationBridgeCryptoMobileFFI.h +703 -0
- package/react-native/generated/swift/GenationBridgeCryptoMobileFFI.modulemap +7 -0
- package/react-native/ios/GenationBridgePeer.m +14 -0
- package/react-native/ios/GenationBridgePeer.swift +234 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/Info.plist +48 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/ios-arm64/Headers/GenationBridgeCryptoMobileFFI.h +703 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/ios-arm64/Headers/module.modulemap +7 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/ios-arm64/libgenation_bridge_crypto_uniffi.a +0 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/ios-arm64_x86_64-simulator/Headers/GenationBridgeCryptoMobileFFI.h +703 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
- package/react-native/native/GenationBridgeCryptoMobile.xcframework/ios-arm64_x86_64-simulator/libgenation_bridge_crypto_uniffi.a +0 -0
- package/react-native.config.js +8 -0
- package/src/core/codec.d.ts +5 -0
- package/src/core/codec.js +46 -0
- package/src/core/errors.d.ts +11 -0
- package/src/core/errors.js +27 -0
- package/src/core/identity.d.ts +16 -0
- package/src/core/identity.js +1 -0
- package/src/core/lifecycle.d.ts +4 -0
- package/src/core/lifecycle.js +1 -0
- package/src/core/mod.d.ts +6 -0
- package/src/core/mod.js +3 -0
- package/src/core/peer.d.ts +67 -0
- package/src/core/peer.js +373 -0
- package/src/core/runtime.d.ts +78 -0
- package/src/core/runtime.js +0 -0
- package/src/node/crypto-core/javascript/mod.d.ts +8 -0
- package/src/node/crypto-core/javascript/mod.js +24 -0
- package/src/node/crypto-core/javascript/provider.d.ts +2 -0
- package/src/node/crypto-core/javascript/provider.js +43 -0
- package/src/node/crypto-core/pkg/wasm/genation_bridge_crypto_wasm.d.ts +122 -0
- package/src/node/crypto-core/pkg/wasm/genation_bridge_crypto_wasm.js +433 -0
- package/src/node/crypto-core/pkg/wasm/genation_bridge_crypto_wasm_bg.wasm +0 -0
- package/src/node/crypto-core/pkg/wasm/genation_bridge_crypto_wasm_bg.wasm.d.ts +25 -0
- package/src/node/encoding.js +1 -0
- package/src/node/file-key-store.d.ts +6 -0
- package/src/node/file-key-store.js +111 -0
- package/src/node/identity-provider.d.ts +8 -0
- package/src/node/identity-provider.js +57 -0
- package/src/node/identity.js +45 -0
- package/src/node/mod.d.ts +16 -0
- package/src/node/mod.js +17 -0
- package/src/node/relay-runtime.d.ts +11 -0
- package/src/node/relay-runtime.js +11 -0
- package/src/react-native/identity-provider.d.ts +4 -0
- package/src/react-native/identity-provider.js +125 -0
- package/src/react-native/lifecycle.d.ts +8 -0
- package/src/react-native/lifecycle.js +9 -0
- package/src/react-native/mod.d.ts +20 -0
- package/src/react-native/mod.js +36 -0
- package/src/react-native/native-module.d.ts +17 -0
- package/src/react-native/native-module.js +9 -0
- package/src/relay/encoding.js +32 -0
- package/src/relay/frame-channel.d.ts +1 -0
- package/src/relay/frame-channel.js +110 -0
- package/src/relay/identity.d.ts +27 -0
- package/src/relay/identity.js +0 -0
- package/src/relay/runtime.d.ts +8 -0
- package/src/relay/runtime.js +900 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { chmod, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ed25519PrivateSigningJwkSchema, opaqueIdSchema } from "@genation/bridge-contracts";
|
|
6
|
+
import { BridgePeerError } from "../core/mod.js";
|
|
7
|
+
const identitySchema = z.object({
|
|
8
|
+
connectionPrivateKey: ed25519PrivateSigningJwkSchema,
|
|
9
|
+
noiseStaticPrivateKey: z.string().length(43).regex(/^[A-Za-z0-9_-]{43}$/)
|
|
10
|
+
}).strict();
|
|
11
|
+
const encodeBase64url = (value)=>btoa(String.fromCharCode(...value)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
12
|
+
const identityName = async (projectId, peerId)=>{
|
|
13
|
+
const bytes = new TextEncoder().encode(`${projectId.length}:${projectId}${peerId.length}:${peerId}`);
|
|
14
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer));
|
|
15
|
+
return encodeBase64url(digest);
|
|
16
|
+
};
|
|
17
|
+
const parseIdentity = (value)=>{
|
|
18
|
+
try {
|
|
19
|
+
const parsed = identitySchema.parse(JSON.parse(value));
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
connectionPrivateKey: Object.freeze({
|
|
22
|
+
...parsed.connectionPrivateKey
|
|
23
|
+
}),
|
|
24
|
+
noiseStaticPrivateKey: parsed.noiseStaticPrivateKey
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
throw new BridgePeerError("KEYSTORE_ERROR");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const hasCode = (error, code)=>error instanceof Error && "code" in error && error.code === code;
|
|
31
|
+
const readExisting = async (path)=>{
|
|
32
|
+
try {
|
|
33
|
+
const value = await readFile(path, "utf8");
|
|
34
|
+
await chmod(path, 0o600);
|
|
35
|
+
return parseIdentity(value);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (hasCode(error, "ENOENT")) return undefined;
|
|
38
|
+
if (error instanceof BridgePeerError) throw error;
|
|
39
|
+
throw new BridgePeerError("KEYSTORE_ERROR");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const waitForIdentity = async (path, deadline)=>{
|
|
43
|
+
while(Date.now() < deadline){
|
|
44
|
+
const existing = await readExisting(path);
|
|
45
|
+
if (existing !== undefined) return existing;
|
|
46
|
+
await new Promise((resolve)=>setTimeout(resolve, 10));
|
|
47
|
+
}
|
|
48
|
+
throw new BridgePeerError("KEYSTORE_ERROR");
|
|
49
|
+
};
|
|
50
|
+
/** Creates the SDK-managed atomic owner-only Node.js and Electron file key store. */ export const createNodeFileKeyStore = (options = {})=>{
|
|
51
|
+
const directory = options.directory ?? join(homedir(), ".genation", "bridge", "peers");
|
|
52
|
+
const lockTimeoutMs = options.lockTimeoutMs ?? 5_000;
|
|
53
|
+
if (directory.length === 0 || !Number.isSafeInteger(lockTimeoutMs) || lockTimeoutMs <= 0) throw new BridgePeerError("INVALID_CONFIGURATION");
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
loadOrCreate: async (input)=>{
|
|
56
|
+
if (!opaqueIdSchema.safeParse(input.projectId).success || !opaqueIdSchema.safeParse(input.peerId).success) throw new BridgePeerError("KEYSTORE_ERROR");
|
|
57
|
+
try {
|
|
58
|
+
await mkdir(directory, {
|
|
59
|
+
recursive: true,
|
|
60
|
+
mode: 0o700
|
|
61
|
+
});
|
|
62
|
+
await chmod(directory, 0o700);
|
|
63
|
+
const name = await identityName(input.projectId, input.peerId);
|
|
64
|
+
const path = join(directory, `${name}.json`);
|
|
65
|
+
const existing = await readExisting(path);
|
|
66
|
+
if (existing !== undefined) return existing;
|
|
67
|
+
const lockPath = join(directory, `${name}.lock`);
|
|
68
|
+
let lock;
|
|
69
|
+
try {
|
|
70
|
+
lock = await open(lockPath, "wx", 0o600);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (hasCode(error, "EEXIST")) {
|
|
73
|
+
return await waitForIdentity(path, Date.now() + lockTimeoutMs);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
let temporary;
|
|
78
|
+
try {
|
|
79
|
+
const afterLock = await readExisting(path);
|
|
80
|
+
if (afterLock !== undefined) return afterLock;
|
|
81
|
+
const created = parseIdentity(JSON.stringify(await input.create()));
|
|
82
|
+
temporary = join(directory, `${name}.${crypto.randomUUID()}.tmp`);
|
|
83
|
+
const file = await open(temporary, "wx", 0o600);
|
|
84
|
+
try {
|
|
85
|
+
await file.writeFile(JSON.stringify(created), "utf8");
|
|
86
|
+
await file.sync();
|
|
87
|
+
} finally{
|
|
88
|
+
await file.close();
|
|
89
|
+
}
|
|
90
|
+
await chmod(temporary, 0o600);
|
|
91
|
+
await rename(temporary, path);
|
|
92
|
+
await chmod(path, 0o600);
|
|
93
|
+
return created;
|
|
94
|
+
} finally{
|
|
95
|
+
if (temporary !== undefined) {
|
|
96
|
+
await rm(temporary, {
|
|
97
|
+
force: true
|
|
98
|
+
}).catch(()=>undefined);
|
|
99
|
+
}
|
|
100
|
+
await lock.close().catch(()=>undefined);
|
|
101
|
+
await rm(lockPath, {
|
|
102
|
+
force: true
|
|
103
|
+
}).catch(()=>undefined);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof BridgePeerError) throw error;
|
|
107
|
+
throw new BridgePeerError("KEYSTORE_ERROR");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BridgeE2eeProvider } from "@genation/bridge-contracts/sdk";
|
|
2
|
+
import type { BridgePeerKeyStore } from "../core/mod.js";
|
|
3
|
+
import type { BridgeRelayIdentityProvider } from "../relay/identity.js";
|
|
4
|
+
/** Node identity-provider dependencies retained outside shared relay orchestration. */ export type NodeRelayIdentityProviderOptions = Readonly<{
|
|
5
|
+
keyStore: BridgePeerKeyStore;
|
|
6
|
+
e2eeProvider?: () => Promise<BridgeE2eeProvider>;
|
|
7
|
+
}>;
|
|
8
|
+
/** Creates the Node file/JWK/WASM implementation of the opaque relay identity port. */ export declare const createNodeRelayIdentityProvider: (options: NodeRelayIdentityProviderOptions) => BridgeRelayIdentityProvider;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { loadBridgeE2eeProvider } from "./crypto-core/javascript/provider.js";
|
|
2
|
+
import { CompactSign, importJWK } from "jose";
|
|
3
|
+
import { decodeBase64url, encodeBase64url } from "../relay/encoding.js";
|
|
4
|
+
import { BridgePeerError } from "../core/mod.js";
|
|
5
|
+
import { createPeerIdentity, derivePeerPublicIdentity } from "./identity.js";
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const signConnectionProof = async (input, privateJwk)=>{
|
|
8
|
+
try {
|
|
9
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(input.grant)));
|
|
10
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
11
|
+
const key = await importJWK(privateJwk, "EdDSA");
|
|
12
|
+
return await new CompactSign(encoder.encode(JSON.stringify({
|
|
13
|
+
version: "proof.v1",
|
|
14
|
+
projectId: input.projectId,
|
|
15
|
+
peerId: input.peerId,
|
|
16
|
+
connectionId: input.connectionId,
|
|
17
|
+
challenge: input.challenge,
|
|
18
|
+
grantSha256: encodeBase64url(digest),
|
|
19
|
+
issuedAt: now,
|
|
20
|
+
expiresAt: Math.min(now + 15, Math.floor(input.challengeExpiresAt / 1_000))
|
|
21
|
+
}))).setProtectedHeader({
|
|
22
|
+
alg: "EdDSA"
|
|
23
|
+
}).sign(key);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new BridgePeerError("GRANT_FAILED");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Creates the Node file/JWK/WASM implementation of the opaque relay identity port. */ export const createNodeRelayIdentityProvider = (options)=>{
|
|
29
|
+
const loadProvider = options.e2eeProvider ?? loadBridgeE2eeProvider;
|
|
30
|
+
let providerPromise;
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
load: async (projectId, peerId)=>{
|
|
33
|
+
const [identity, provider] = await Promise.all([
|
|
34
|
+
options.keyStore.loadOrCreate({
|
|
35
|
+
projectId,
|
|
36
|
+
peerId,
|
|
37
|
+
create: createPeerIdentity
|
|
38
|
+
}),
|
|
39
|
+
providerPromise ??= loadProvider()
|
|
40
|
+
]);
|
|
41
|
+
const publicIdentity = derivePeerPublicIdentity(identity, provider);
|
|
42
|
+
const localPrivateKey = decodeBase64url(identity.noiseStaticPrivateKey);
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
publicIdentity,
|
|
45
|
+
signConnectionProof: (input)=>signConnectionProof(input, identity.connectionPrivateKey),
|
|
46
|
+
createInitiator: (input)=>provider.createInitiator({
|
|
47
|
+
...input,
|
|
48
|
+
localPrivateKey
|
|
49
|
+
}),
|
|
50
|
+
createResponder: (input)=>provider.createResponder({
|
|
51
|
+
...input,
|
|
52
|
+
localPrivateKey
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ed25519PrivateSigningJwkSchema } from "@genation/bridge-contracts";
|
|
2
|
+
import { exportJWK } from "jose";
|
|
3
|
+
import { BridgePeerError } from "../core/mod.js";
|
|
4
|
+
import { decodeBase64url, encodeBase64url } from "./encoding.js";
|
|
5
|
+
/** Generates one complete persistent peer identity using Web Crypto randomness. */ export const createPeerIdentity = async ()=>{
|
|
6
|
+
try {
|
|
7
|
+
const pair = await crypto.subtle.generateKey({
|
|
8
|
+
name: "Ed25519"
|
|
9
|
+
}, true, [
|
|
10
|
+
"sign",
|
|
11
|
+
"verify"
|
|
12
|
+
]);
|
|
13
|
+
const privateJwk = ed25519PrivateSigningJwkSchema.parse(await exportJWK(pair.privateKey));
|
|
14
|
+
const noiseStaticPrivateKey = encodeBase64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
connectionPrivateKey: Object.freeze({
|
|
17
|
+
...privateJwk
|
|
18
|
+
}),
|
|
19
|
+
noiseStaticPrivateKey
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
throw new BridgePeerError("KEYSTORE_ERROR");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Derives only public grant input from one validated local private identity. */ export const derivePeerPublicIdentity = (identity, e2ee)=>{
|
|
26
|
+
try {
|
|
27
|
+
const privateKey = ed25519PrivateSigningJwkSchema.parse(identity.connectionPrivateKey);
|
|
28
|
+
const noisePrivate = decodeBase64url(identity.noiseStaticPrivateKey);
|
|
29
|
+
if (noisePrivate.length !== 32) throw new Error("invalid_noise_key");
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
connectionPublicKey: Object.freeze({
|
|
32
|
+
kty: "OKP",
|
|
33
|
+
crv: "Ed25519",
|
|
34
|
+
x: privateKey.x
|
|
35
|
+
}),
|
|
36
|
+
noiseStaticPublicKey: Object.freeze({
|
|
37
|
+
kty: "OKP",
|
|
38
|
+
crv: "X25519",
|
|
39
|
+
x: encodeBase64url(e2ee.deriveStaticPublic(noisePrivate))
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
throw new BridgePeerError("KEYSTORE_ERROR");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BridgePeer, type BridgePeerCoreOptions, type BridgePeerKeyStore } from "../core/mod.js";
|
|
2
|
+
import { type NodeFileKeyStoreOptions } from "./file-key-store.js";
|
|
3
|
+
import { type NodeRelayRuntimeOptions } from "./relay-runtime.js";
|
|
4
|
+
/** Default managed relay endpoint used by the Node.js happy path. */ export declare const defaultBridgeRelayUrl: "wss://relay.genation.com/v1/relay";
|
|
5
|
+
/** Node.js and Electron peer options layered on the shared public facade. */ export type BridgePeerOptions = BridgePeerCoreOptions & Readonly<{
|
|
6
|
+
relayUrl?: string;
|
|
7
|
+
keyStore?: BridgePeerKeyStore;
|
|
8
|
+
fileKeyStore?: NodeFileKeyStoreOptions;
|
|
9
|
+
reconnect?: BridgePeerCoreOptions["reconnect"];
|
|
10
|
+
runtime?: Omit<NodeRelayRuntimeOptions, "relayUrl" | "keyStore">;
|
|
11
|
+
}>;
|
|
12
|
+
/** Creates one Node.js or Electron-main peer with SDK-managed platform defaults. */ export declare const createBridgePeer: (options: BridgePeerOptions) => BridgePeer;
|
|
13
|
+
export * from "../core/mod.js";
|
|
14
|
+
export { createNodeFileKeyStore, type NodeFileKeyStoreOptions } from "./file-key-store.js";
|
|
15
|
+
export { createNodeRelayIdentityProvider, type NodeRelayIdentityProviderOptions } from "./identity-provider.js";
|
|
16
|
+
export { createNodeRelayRuntime, type NodeRelayRuntimeOptions } from "./relay-runtime.js";
|
package/src/node/mod.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createBridgePeerCore } from "../core/mod.js";
|
|
2
|
+
import { createNodeFileKeyStore } from "./file-key-store.js";
|
|
3
|
+
import { createNodeRelayRuntime } from "./relay-runtime.js";
|
|
4
|
+
/** Default managed relay endpoint used by the Node.js happy path. */ export const defaultBridgeRelayUrl = "wss://relay.genation.com/v1/relay";
|
|
5
|
+
/** Creates one Node.js or Electron-main peer with SDK-managed platform defaults. */ export const createBridgePeer = (options)=>{
|
|
6
|
+
const keyStore = options.keyStore ?? createNodeFileKeyStore(options.fileKeyStore);
|
|
7
|
+
const runtime = createNodeRelayRuntime({
|
|
8
|
+
relayUrl: options.relayUrl ?? defaultBridgeRelayUrl,
|
|
9
|
+
keyStore,
|
|
10
|
+
...options.runtime
|
|
11
|
+
});
|
|
12
|
+
return createBridgePeerCore(options, runtime);
|
|
13
|
+
};
|
|
14
|
+
export * from "../core/mod.js";
|
|
15
|
+
export { createNodeFileKeyStore } from "./file-key-store.js";
|
|
16
|
+
export { createNodeRelayIdentityProvider } from "./identity-provider.js";
|
|
17
|
+
export { createNodeRelayRuntime } from "./relay-runtime.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BridgeE2eeProvider } from "@genation/bridge-contracts/sdk";
|
|
2
|
+
import type { BridgePeerKeyStore, BridgePeerRuntime } from "../core/mod.js";
|
|
3
|
+
import { type BridgeRelayRuntimeOptions } from "../relay/runtime.js";
|
|
4
|
+
/** Node relay dependencies selected without runtime platform detection. */ export type NodeRelayRuntimeOptions = Readonly<{
|
|
5
|
+
relayUrl: string;
|
|
6
|
+
keyStore: BridgePeerKeyStore;
|
|
7
|
+
webSocketFactory?: BridgeRelayRuntimeOptions["webSocketFactory"];
|
|
8
|
+
e2eeProvider?: () => Promise<BridgeE2eeProvider>;
|
|
9
|
+
operationTimeoutMs?: number;
|
|
10
|
+
}>;
|
|
11
|
+
/** Creates the normative Node relay runtime around file storage and shared E2EE. */ export declare const createNodeRelayRuntime: (options: NodeRelayRuntimeOptions) => BridgePeerRuntime;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createRelayRuntime } from "../relay/runtime.js";
|
|
2
|
+
import { createNodeRelayIdentityProvider } from "./identity-provider.js";
|
|
3
|
+
/** Creates the normative Node relay runtime around file storage and shared E2EE. */ export const createNodeRelayRuntime = (options)=>createRelayRuntime({
|
|
4
|
+
relayUrl: options.relayUrl,
|
|
5
|
+
identityProvider: createNodeRelayIdentityProvider({
|
|
6
|
+
keyStore: options.keyStore,
|
|
7
|
+
e2eeProvider: options.e2eeProvider
|
|
8
|
+
}),
|
|
9
|
+
webSocketFactory: options.webSocketFactory,
|
|
10
|
+
operationTimeoutMs: options.operationTimeoutMs
|
|
11
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BridgeRelayIdentityProvider } from "../relay/identity.js";
|
|
2
|
+
import type { GenationBridgeNativeModule } from "./native-module.js";
|
|
3
|
+
export type { GenationBridgeNativeModule } from "./native-module.js";
|
|
4
|
+
/** Adapts secure native identity handles to the shared relay identity port. */ export declare const createReactNativeIdentityProvider: (module: GenationBridgeNativeModule) => BridgeRelayIdentityProvider;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { BridgePeerError } from "../core/mod.js";
|
|
2
|
+
import { decodeBase64url, encodeBase64url } from "../relay/encoding.js";
|
|
3
|
+
const keyPattern = /^[A-Za-z0-9_-]{43}$/;
|
|
4
|
+
const handlePattern = /^[A-Za-z0-9._:-]{1,512}$/;
|
|
5
|
+
const transportFailure = ()=>new BridgePeerError("TRANSPORT_ERROR");
|
|
6
|
+
const createInitiator = (module, identityHandle, input)=>{
|
|
7
|
+
let handle;
|
|
8
|
+
try {
|
|
9
|
+
handle = module.createInitiator(identityHandle, encodeBase64url(input.prologue), encodeBase64url(input.responderPublicKey));
|
|
10
|
+
if (!handlePattern.test(handle)) throw new Error("invalid_handle");
|
|
11
|
+
} catch {
|
|
12
|
+
throw transportFailure();
|
|
13
|
+
}
|
|
14
|
+
let closed = false;
|
|
15
|
+
const use = (operation)=>{
|
|
16
|
+
if (closed) throw transportFailure();
|
|
17
|
+
try {
|
|
18
|
+
return operation();
|
|
19
|
+
} catch {
|
|
20
|
+
closed = true;
|
|
21
|
+
try {
|
|
22
|
+
module.closeSession(handle);
|
|
23
|
+
} catch {
|
|
24
|
+
// The application never receives provider-specific native cleanup details.
|
|
25
|
+
}
|
|
26
|
+
throw transportFailure();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const session = {
|
|
30
|
+
beginHandshake: ()=>use(()=>decodeBase64url(module.beginHandshake(handle))),
|
|
31
|
+
finishHandshake: (message)=>use(()=>{
|
|
32
|
+
if (!module.finishHandshake(handle, encodeBase64url(message))) {
|
|
33
|
+
throw new Error("handshake_failed");
|
|
34
|
+
}
|
|
35
|
+
}),
|
|
36
|
+
encrypt: (aad, plaintext)=>use(()=>decodeBase64url(module.encrypt(handle, encodeBase64url(aad), encodeBase64url(plaintext)))),
|
|
37
|
+
decrypt: (aad, ciphertext)=>use(()=>decodeBase64url(module.decrypt(handle, encodeBase64url(aad), encodeBase64url(ciphertext)))),
|
|
38
|
+
close: ()=>{
|
|
39
|
+
if (closed) return;
|
|
40
|
+
closed = true;
|
|
41
|
+
try {
|
|
42
|
+
module.closeSession(handle);
|
|
43
|
+
} catch {
|
|
44
|
+
// Close is idempotent and doesn't expose native provider diagnostics.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return Object.freeze(session);
|
|
49
|
+
};
|
|
50
|
+
const createResponder = (module, identityHandle, input)=>{
|
|
51
|
+
let handle;
|
|
52
|
+
try {
|
|
53
|
+
handle = module.createResponder(identityHandle, encodeBase64url(input.prologue), encodeBase64url(input.initiatorPublicKey));
|
|
54
|
+
if (!handlePattern.test(handle)) throw new Error("invalid_handle");
|
|
55
|
+
} catch {
|
|
56
|
+
throw transportFailure();
|
|
57
|
+
}
|
|
58
|
+
let closed = false;
|
|
59
|
+
const use = (operation)=>{
|
|
60
|
+
if (closed) throw transportFailure();
|
|
61
|
+
try {
|
|
62
|
+
return operation();
|
|
63
|
+
} catch {
|
|
64
|
+
closed = true;
|
|
65
|
+
try {
|
|
66
|
+
module.closeSession(handle);
|
|
67
|
+
} catch {
|
|
68
|
+
// The application never receives provider-specific native cleanup details.
|
|
69
|
+
}
|
|
70
|
+
throw transportFailure();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const session = {
|
|
74
|
+
acceptHandshake: (message)=>use(()=>decodeBase64url(module.acceptHandshake(handle, encodeBase64url(message)))),
|
|
75
|
+
encrypt: (aad, plaintext)=>use(()=>decodeBase64url(module.encrypt(handle, encodeBase64url(aad), encodeBase64url(plaintext)))),
|
|
76
|
+
decrypt: (aad, ciphertext)=>use(()=>decodeBase64url(module.decrypt(handle, encodeBase64url(aad), encodeBase64url(ciphertext)))),
|
|
77
|
+
close: ()=>{
|
|
78
|
+
if (closed) return;
|
|
79
|
+
closed = true;
|
|
80
|
+
try {
|
|
81
|
+
module.closeSession(handle);
|
|
82
|
+
} catch {
|
|
83
|
+
// Close is idempotent and doesn't expose native provider diagnostics.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return Object.freeze(session);
|
|
88
|
+
};
|
|
89
|
+
/** Adapts secure native identity handles to the shared relay identity port. */ export const createReactNativeIdentityProvider = (module)=>Object.freeze({
|
|
90
|
+
load: (projectId, peerId)=>{
|
|
91
|
+
try {
|
|
92
|
+
const identity = module.loadOrCreateIdentity(projectId, peerId);
|
|
93
|
+
if (!handlePattern.test(identity.identityHandle) || !keyPattern.test(identity.connectionPublicKey) || !keyPattern.test(identity.noiseStaticPublicKey)) throw new Error("invalid_native_identity");
|
|
94
|
+
const identityHandle = identity.identityHandle;
|
|
95
|
+
return Promise.resolve(Object.freeze({
|
|
96
|
+
publicIdentity: Object.freeze({
|
|
97
|
+
connectionPublicKey: Object.freeze({
|
|
98
|
+
kty: "OKP",
|
|
99
|
+
crv: "Ed25519",
|
|
100
|
+
x: identity.connectionPublicKey
|
|
101
|
+
}),
|
|
102
|
+
noiseStaticPublicKey: Object.freeze({
|
|
103
|
+
kty: "OKP",
|
|
104
|
+
crv: "X25519",
|
|
105
|
+
x: identity.noiseStaticPublicKey
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
signConnectionProof: (input)=>{
|
|
109
|
+
try {
|
|
110
|
+
const proof = module.signConnectionProof(identityHandle, JSON.stringify(input));
|
|
111
|
+
if (proof.split(".").length !== 3) throw new Error("invalid_proof");
|
|
112
|
+
return Promise.resolve(proof);
|
|
113
|
+
} catch {
|
|
114
|
+
return Promise.reject(new BridgePeerError("GRANT_FAILED"));
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
createInitiator: (input)=>createInitiator(module, identityHandle, input),
|
|
118
|
+
createResponder: (input)=>createResponder(module, identityHandle, input)
|
|
119
|
+
}));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof BridgePeerError) return Promise.reject(error);
|
|
122
|
+
return Promise.reject(new BridgePeerError("KEYSTORE_ERROR"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BridgePeerPlatformLifecycle } from "../core/mod.js";
|
|
2
|
+
/** Minimal React Native AppState contract consumed by the platform adapter. */ export type ReactNativeAppState = Readonly<{
|
|
3
|
+
readonly currentState: string;
|
|
4
|
+
addEventListener(event: "change", listener: (state: string) => void): Readonly<{
|
|
5
|
+
remove(): void;
|
|
6
|
+
}>;
|
|
7
|
+
}>;
|
|
8
|
+
/** Maps React Native AppState into the core's active versus suspended lifecycle. */ export declare const createReactNativeLifecycle: (appState: ReactNativeAppState) => BridgePeerPlatformLifecycle;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Maps React Native AppState into the core's active versus suspended lifecycle. */ export const createReactNativeLifecycle = (appState)=>Object.freeze({
|
|
2
|
+
get active () {
|
|
3
|
+
return appState.currentState === "active";
|
|
4
|
+
},
|
|
5
|
+
subscribe: (listener)=>{
|
|
6
|
+
const subscription = appState.addEventListener("change", (state)=>listener(state === "active"));
|
|
7
|
+
return ()=>subscription.remove();
|
|
8
|
+
}
|
|
9
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type BridgePeer, type BridgePeerCoreOptions, type BridgePeerPlatformLifecycle } from "../core/mod.js";
|
|
2
|
+
import type { BridgeRelayIdentityProvider } from "../relay/identity.js";
|
|
3
|
+
import type { BridgeWebSocketFactory } from "../relay/frame-channel.js";
|
|
4
|
+
import { type ReactNativeAppState } from "./lifecycle.js";
|
|
5
|
+
import { type GenationBridgeNativeModule } from "./native-module.js";
|
|
6
|
+
/** Default managed relay endpoint used by the React Native happy path. */ export declare const defaultBridgeRelayUrl: "wss://relay.genation.com/v1/relay";
|
|
7
|
+
/** React Native options layered on the shared public peer facade. */ export type BridgePeerOptions = BridgePeerCoreOptions & Readonly<{
|
|
8
|
+
relayUrl?: string;
|
|
9
|
+
nativeModule?: GenationBridgeNativeModule;
|
|
10
|
+
appState?: ReactNativeAppState;
|
|
11
|
+
identityProvider?: BridgeRelayIdentityProvider;
|
|
12
|
+
platformLifecycle?: BridgePeerPlatformLifecycle;
|
|
13
|
+
webSocketFactory?: BridgeWebSocketFactory;
|
|
14
|
+
operationTimeoutMs?: number;
|
|
15
|
+
}>;
|
|
16
|
+
/** Creates one foreground React Native peer through the conditional mobile adapter. */ export declare const createBridgePeer: (options: BridgePeerOptions) => BridgePeer;
|
|
17
|
+
export * from "../core/mod.js";
|
|
18
|
+
export { createReactNativeIdentityProvider, type GenationBridgeNativeModule } from "./identity-provider.js";
|
|
19
|
+
export { createReactNativeLifecycle } from "./lifecycle.js";
|
|
20
|
+
export { type ReactNativeAppState } from "./lifecycle.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BridgePeerError, createBridgePeerCore } from "../core/mod.js";
|
|
2
|
+
import { createRelayRuntime } from "../relay/runtime.js";
|
|
3
|
+
import { createReactNativeIdentityProvider } from "./identity-provider.js";
|
|
4
|
+
import { createReactNativeLifecycle } from "./lifecycle.js";
|
|
5
|
+
import { loadReactNative, resolveGenationBridgeNativeModule } from "./native-module.js";
|
|
6
|
+
/** Default managed relay endpoint used by the React Native happy path. */ export const defaultBridgeRelayUrl = "wss://relay.genation.com/v1/relay";
|
|
7
|
+
const validateRelayUrl = (value)=>{
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
|
|
11
|
+
if (url.protocol !== "wss:" && !(url.protocol === "ws:" && loopback)) {
|
|
12
|
+
throw new Error("insecure_relay");
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
throw new BridgePeerError("INVALID_CONFIGURATION");
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/** Creates one foreground React Native peer through the conditional mobile adapter. */ export const createBridgePeer = (options)=>{
|
|
19
|
+
const relayUrl = options.relayUrl ?? defaultBridgeRelayUrl;
|
|
20
|
+
validateRelayUrl(relayUrl);
|
|
21
|
+
const native = options.nativeModule ?? (options.identityProvider === undefined ? resolveGenationBridgeNativeModule() : undefined);
|
|
22
|
+
const identityProvider = options.identityProvider ?? createReactNativeIdentityProvider(native);
|
|
23
|
+
const lifecycle = options.platformLifecycle ?? createReactNativeLifecycle(options.appState ?? loadReactNative().AppState);
|
|
24
|
+
return createBridgePeerCore({
|
|
25
|
+
...options,
|
|
26
|
+
lifecycle
|
|
27
|
+
}, createRelayRuntime({
|
|
28
|
+
relayUrl,
|
|
29
|
+
identityProvider,
|
|
30
|
+
webSocketFactory: options.webSocketFactory,
|
|
31
|
+
operationTimeoutMs: options.operationTimeoutMs
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
34
|
+
export * from "../core/mod.js";
|
|
35
|
+
export { createReactNativeIdentityProvider } from "./identity-provider.js";
|
|
36
|
+
export { createReactNativeLifecycle } from "./lifecycle.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Public-only identity descriptor returned by native secure storage. */ export type NativePeerIdentity = Readonly<{
|
|
2
|
+
identityHandle: string;
|
|
3
|
+
connectionPublicKey: string;
|
|
4
|
+
noiseStaticPublicKey: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** Synchronous native boundary required by the foreground mobile adapter. */ export type GenationBridgeNativeModule = {
|
|
7
|
+
loadOrCreateIdentity(projectId: string, peerId: string): NativePeerIdentity;
|
|
8
|
+
signConnectionProof(identityHandle: string, inputJson: string): string;
|
|
9
|
+
createInitiator(identityHandle: string, prologue: string, responderPublicKey: string): string;
|
|
10
|
+
createResponder(identityHandle: string, prologue: string, initiatorPublicKey: string): string;
|
|
11
|
+
beginHandshake(sessionHandle: string): string;
|
|
12
|
+
finishHandshake(sessionHandle: string, message: string): boolean;
|
|
13
|
+
acceptHandshake(sessionHandle: string, message: string): string;
|
|
14
|
+
encrypt(sessionHandle: string, aad: string, plaintext: string): string;
|
|
15
|
+
decrypt(sessionHandle: string, aad: string, ciphertext: string): string;
|
|
16
|
+
closeSession(sessionHandle: string): unknown;
|
|
17
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BridgePeerError } from "../core/mod.js";
|
|
2
|
+
/** Resolves the native host selected by the React Native conditional export. */ export const resolveGenationBridgeNativeModule = ()=>{
|
|
3
|
+
const installed = globalThis.__genationBridgePeerNative;
|
|
4
|
+
if (installed !== undefined) return installed;
|
|
5
|
+
const module = loadReactNative().NativeModules.GenationBridgePeer;
|
|
6
|
+
if (module === undefined) throw new BridgePeerError("KEYSTORE_ERROR");
|
|
7
|
+
return module;
|
|
8
|
+
};
|
|
9
|
+
/** Loads the conditional host dependency without probing process or user agent state. */ export const loadReactNative = ()=>require("react-native");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { peerGrantClaimsSchema } from "@genation/bridge-contracts";
|
|
2
|
+
import { BridgePeerError } from "../core/mod.js";
|
|
3
|
+
/** Encodes bytes as canonical unpadded base64url. */ export const encodeBase64url = (value)=>{
|
|
4
|
+
const pieces = [];
|
|
5
|
+
const chunkSize = 0x8000;
|
|
6
|
+
for(let offset = 0; offset < value.length; offset += chunkSize){
|
|
7
|
+
pieces.push(String.fromCharCode(...value.subarray(offset, offset + chunkSize)));
|
|
8
|
+
}
|
|
9
|
+
return btoa(pieces.join("")).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
10
|
+
};
|
|
11
|
+
/** Decodes canonical unpadded base64url or throws a sanitized transport error. */ export const decodeBase64url = (value)=>{
|
|
12
|
+
try {
|
|
13
|
+
const padding = "=".repeat((4 - value.length % 4) % 4);
|
|
14
|
+
const bytes = Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/") + padding), (character)=>character.charCodeAt(0));
|
|
15
|
+
if (encodeBase64url(bytes) !== value) throw new Error("noncanonical");
|
|
16
|
+
return bytes;
|
|
17
|
+
} catch {
|
|
18
|
+
throw new BridgePeerError("TRANSPORT_ERROR");
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/** Parses public grant claims without treating the peer as a signature authority. */ export const parseRelayGrantClaims = (grant)=>{
|
|
22
|
+
try {
|
|
23
|
+
const segments = grant.split(".");
|
|
24
|
+
if (segments.length !== 3) throw new Error("invalid_compact");
|
|
25
|
+
const payload = new TextDecoder("utf-8", {
|
|
26
|
+
fatal: true
|
|
27
|
+
}).decode(decodeBase64url(segments[1]));
|
|
28
|
+
return peerGrantClaimsSchema.parse(JSON.parse(payload));
|
|
29
|
+
} catch {
|
|
30
|
+
throw new BridgePeerError("GRANT_FAILED");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/** WebSocket constructor boundary used by Node tests and platform composition. */ export type BridgeWebSocketFactory = (url: string) => WebSocket;
|