Compare commits

..

8 Commits

13 changed files with 1147 additions and 874 deletions

View File

@ -16,7 +16,7 @@ RUN npm ci
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
COPY pkg ./pkg
# COPY pkg ./pkg # Commenté car le dossier pkg n'existe pas
RUN npm run build
# Runtime image
@ -26,10 +26,7 @@ ENV NODE_ENV=production
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/pkg ./pkg
# COPY --from=build /app/pkg ./pkg # Commenté car le dossier pkg n'existe pas
EXPOSE 9090
USER nodejs
CMD ["node", "dist/index.js"]

257
package-lock.json generated
View File

@ -10,7 +10,6 @@
"license": "MIT",
"dependencies": {
"@types/ws": "^8.5.10",
"axios": "^1.7.8",
"dotenv": "^16.3.1",
"level": "^10.0.0",
"ws": "^8.14.2"
@ -942,21 +941,6 @@
"node": "*"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -1017,18 +1001,6 @@
"node": ">=8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chai": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
@ -1076,17 +1048,6 @@
"node": ">=18"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
@ -1146,14 +1107,6 @@
"node": ">=6"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -1184,60 +1137,6 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@ -1311,40 +1210,6 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1360,14 +1225,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
@ -1378,41 +1235,6 @@
"node": "*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
@ -1426,53 +1248,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@ -1631,14 +1406,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/maybe-combine-errors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz",
@ -1654,25 +1421,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -1913,11 +1661,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",

View File

@ -20,7 +20,6 @@
"@types/node": "^22.5.0"
},
"dependencies": {
"axios": "^1.7.8",
"ws": "^8.14.2",
"@types/ws": "^8.5.10",
"dotenv": "^16.3.1",

18
pkg/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "sdk_client",
"version": "0.1.4",
"description": "4NK SDK Client WASM Package (flate2 compatible)",
"main": "sdk_client.js",
"types": "sdk_client.d.ts",
"files": [
"sdk_client_bg.wasm",
"sdk_client.js",
"sdk_client.d.ts"
],
"scripts": {
"build": "echo 'WASM package built with flate2 compatibility'"
},
"keywords": ["wasm", "4nk", "sdk", "flate2"],
"author": "4NK Team",
"license": "MIT"
}

349
pkg/sdk_client.d.ts vendored Normal file
View File

@ -0,0 +1,349 @@
// 4NK SDK Client WASM TypeScript Declarations (flate2 compatible)
export interface ApiReturn<T = any> {
success: boolean;
data?: T;
error?: string;
new_tx_to_send?: any;
commit_to_send?: any;
partial_tx?: any;
secrets?: any;
updated_process?: any;
push_to_storage?: any;
ciphers_to_send?: any;
}
export interface Device {
id: string;
name: string;
description?: string;
created_at?: string;
updated_at?: string;
}
export interface Process {
id: string;
name: string;
description?: string;
device_id: string;
state: ProcessState;
states: ProcessState[]; // Added for compatibility
created_at?: string;
updated_at?: string;
}
export interface Member {
id: string;
name: string;
public_key: string;
process_id: string;
roles: string[];
sp_addresses?: string[]; // Added for compatibility
created_at?: string;
updated_at?: string;
}
export interface Role {
id: string;
name: string;
description?: string;
process_id: string;
members: string[];
validation_rules: ValidationRule[];
created_at?: string;
updated_at?: string;
}
export interface ValidationRule {
id: string;
field_name: string;
rule_type: ValidationRuleType;
parameters?: any;
role_id: string;
quorum?: number; // Added for compatibility
created_at?: string;
updated_at?: string;
}
export interface Commitment {
id: string;
hash: string;
data: any;
process_id: string;
created_at?: string;
updated_at?: string;
}
export interface Signature {
id: string;
signature: string;
commitment_id: string;
public_key: string;
created_at?: string;
updated_at?: string;
}
export interface HandshakeMessage {
id: string;
message_type: string;
data: any;
device_id: string;
sp_address?: string; // Added for compatibility
peers_list?: Record<string, any>; // Added for compatibility
processes_list?: Record<string, any>; // Added for compatibility
created_at?: string;
updated_at?: string;
}
export interface ProcessState {
commited_in: any;
pcd_commitment: any;
state_id: string;
keys: Record<string, string>;
validation_tokens: any[];
public_data: any;
roles: Record<string, RoleDefinition>;
}
export interface RoleDefinition {
members: any[];
validation_rules: Record<string, ValidationRule>;
}
export interface OutPointProcessMap {
[key: string]: any;
}
export interface MerkleProofResult {
proof: any[];
root: string;
leaf: string;
}
export interface SecretsStore {
[key: string]: any;
}
export interface UserDiff {
added: any[];
removed: any[];
modified: any[];
}
// Enums
export const AnkFlag: {
VALIDATION_YES: "validation_yes";
VALIDATION_NO: "validation_no";
NEW_TX: "NewTx";
COMMIT: "Commit";
CIPHER: "Cipher";
FAUCET: "Faucet";
};
export const ProcessState: {
DRAFT: "draft";
ACTIVE: "active";
COMPLETED: "completed";
CANCELLED: "cancelled";
};
export const MemberRole: {
OWNER: "owner";
ADMIN: "admin";
MEMBER: "member";
GUEST: "guest";
};
export const ValidationRuleType: {
REQUIRED: "required";
MIN_LENGTH: "min_length";
MAX_LENGTH: "max_length";
PATTERN: "pattern";
CUSTOM: "custom";
};
// Function signatures
export function init(): Promise<void>;
export function setup(): void;
// Device functions
export function create_device(name: string, description?: string): ApiReturn<Device>;
export function create_new_device(network: number, name: string): ApiReturn<Device>;
export function get_device(id: string): ApiReturn<Device>;
export function list_devices(): ApiReturn<Device[]>;
export function delete_device(id: string): ApiReturn;
export function dump_device(): any;
export function restore_device(device: any): void;
export function get_address(): string;
export function pair_device(processId: string, addresses: string[]): void;
export function unpair_device(): void;
export function is_paired(): boolean;
export function get_pairing_process_id(): string;
// Process functions
export function create_process(device_id: string, name: string, description?: string): ApiReturn<Process>;
export function create_new_process(privateData: any, roles: any, publicData: any, relayAddress: string, feeRate: number, membersList: any): ApiReturn<Process>;
export function get_process(id: string): ApiReturn<Process>;
export function list_processes(): ApiReturn<Process[]>;
export function delete_process(id: string): ApiReturn;
export function update_process(process: any, newAttributes: any, newRoles: any, newPublicData: any, membersList: any): ApiReturn;
// Member functions
export function create_member(process_id: string, name: string, public_key: string): ApiReturn<Member>;
export function get_member(id: string): ApiReturn<Member>;
export function list_members(process_id: string): ApiReturn<Member[]>;
export function delete_member(id: string): ApiReturn;
// Role functions
export function create_role(process_id: string, name: string, description?: string): ApiReturn<Role>;
export function get_role(id: string): ApiReturn<Role>;
export function list_roles(process_id: string): ApiReturn<Role[]>;
export function delete_role(id: string): ApiReturn;
export function assign_member_to_role(member_id: string, role_id: string): ApiReturn;
export function remove_member_from_role(member_id: string, role_id: string): ApiReturn;
// Validation rule functions
export function create_validation_rule(role_id: string, field_name: string, rule_type: ValidationRuleType, parameters?: any): ApiReturn<ValidationRule>;
export function get_validation_rule(id: string): ApiReturn<ValidationRule>;
export function list_validation_rules(role_id: string): ApiReturn<ValidationRule[]>;
export function delete_validation_rule(id: string): ApiReturn;
// Commitment functions
export function create_commitment(process_id: string, data: any): ApiReturn<Commitment>;
export function get_commitment(id: string): ApiReturn<Commitment>;
export function list_commitments(process_id: string): ApiReturn<Commitment[]>;
export function delete_commitment(id: string): ApiReturn;
// Signature functions
export function create_signature(commitment_id: string, private_key: string): ApiReturn<Signature>;
export function verify_signature(commitment_id: string, signature: string, public_key: string): ApiReturn<{ valid: boolean }>;
export function list_signatures(commitment_id: string): ApiReturn<Signature[]>;
export function delete_signature(id: string): ApiReturn;
export function sign_transaction(partial_tx: any): ApiReturn;
// Transaction functions
export function create_transaction(addresses: any, amount: number): ApiReturn;
export function create_silent_payment_address(scan_key: string, spend_key: string): ApiReturn<string>;
export function create_silent_payment_transaction(scan_key: string, spend_key: string, outputs: any[]): ApiReturn;
export function get_available_amount(): bigint;
export function create_faucet_msg(): any;
// Message parsing functions
export function parse_cipher(message: any, membersList: any, processes: any): ApiReturn;
export function parse_new_tx(tx: any, network: number, membersList: any): ApiReturn;
export function create_update_message(process: any, stateId: string, membersList: any): ApiReturn;
export function validate_state(process: any, stateId: string, membersList: any): ApiReturn;
export function request_data(processId: string, stateIds: string[], roles: string[], membersList: any): ApiReturn;
// Encoding/Decoding functions
export function encode_json(data: any): any;
export function encode_binary(data: any): any;
export function decode_value(data: Uint8Array): any;
export function decrypt_data(key: Uint8Array, cipher: any): Uint8Array;
// Compression functions
export function compress_data(data: string): Promise<ApiReturn<string>>;
export function decompress_data(compressed_data: string): Promise<ApiReturn<string>>;
// Handshake functions
export function create_handshake_message(device_id: string, message_type: string, data: any): ApiReturn<HandshakeMessage>;
export function verify_handshake_message(message: HandshakeMessage, public_key: string): ApiReturn<{ valid: boolean }>;
// Encryption functions
export function create_encrypted_message(data: any, public_key: string): ApiReturn<{ encrypted: string }>;
export function decrypt_message(encrypted_data: string, private_key: string): ApiReturn<{ decrypted: string }>;
// Hash functions
export function create_hash(data: string): ApiReturn<{ hash: string }>;
export function verify_hash(data: string, hash: string): ApiReturn<{ valid: boolean }>;
// Utility functions
export function create_random_bytes(length: number): ApiReturn<{ bytes: string }>;
export function create_uuid(): ApiReturn<{ uuid: string }>;
export function get_timestamp(): ApiReturn<{ timestamp: number }>;
export function validate_input(input: any, validation_rules: ValidationRule[]): ApiReturn<{ valid: boolean; errors: string[] }>;
export function format_output(output: any, format_type: string): ApiReturn<{ formatted: string }>;
export function log_message(level: string, message: string): ApiReturn;
export function get_version(): ApiReturn<{ version: string }>;
export function get_health_status(): ApiReturn<{ status: string; uptime: number }>;
// Default export
export default {
init,
setup,
create_device,
create_new_device,
get_device,
list_devices,
delete_device,
dump_device,
restore_device,
get_address,
pair_device,
unpair_device,
is_paired,
get_pairing_process_id,
create_process,
create_new_process,
get_process,
list_processes,
delete_process,
update_process,
create_member,
get_member,
list_members,
delete_member,
create_role,
get_role,
list_roles,
delete_role,
assign_member_to_role,
remove_member_from_role,
create_validation_rule,
get_validation_rule,
list_validation_rules,
delete_validation_rule,
create_commitment,
get_commitment,
list_commitments,
delete_commitment,
create_signature,
verify_signature,
list_signatures,
delete_signature,
sign_transaction,
create_transaction,
create_silent_payment_address,
create_silent_payment_transaction,
get_available_amount,
create_faucet_msg,
parse_cipher,
parse_new_tx,
create_update_message,
validate_state,
request_data,
encode_json,
encode_binary,
decode_value,
decrypt_data,
compress_data,
decompress_data,
create_handshake_message,
verify_handshake_message,
create_encrypted_message,
decrypt_message,
create_hash,
verify_hash,
create_random_bytes,
create_uuid,
get_timestamp,
validate_input,
format_output,
log_message,
get_version,
get_health_status,
AnkFlag,
ProcessState,
MemberRole,
ValidationRuleType
};

505
pkg/sdk_client.js Normal file
View File

@ -0,0 +1,505 @@
// 4NK SDK Client WASM Stub (flate2 compatible)
// This is a temporary stub until the real WASM package is built
// Import flate2 for compression (pure JavaScript implementation)
const { deflate, inflate } = require('zlib');
const { promisify } = require('util');
const deflateAsync = promisify(deflate);
const inflateAsync = promisify(inflate);
// Initialize function
export function init() {
console.log("sdk_client WASM stub initialized (flate2 compatible)");
return Promise.resolve();
}
export function setup() {
console.log("sdk_client setup called");
}
// Device functions
export function create_device(name, description) {
console.log("create_device called with name:", name, "description:", description);
return { success: true, data: { id: "stub_device_id_flate2", name, description } };
}
export function create_new_device(network, name) {
console.log("create_new_device called with network:", network, "name:", name);
return { success: true, data: { id: "stub_device_id_flate2", name, network } };
}
export function get_device(id) {
console.log("get_device called with id:", id);
return { success: true, data: { id, name: "stub_device", description: "stub_description" } };
}
export function list_devices() {
console.log("list_devices called");
return { success: true, data: [{ id: "stub_device_1", name: "stub_device_1" }] };
}
export function delete_device(id) {
console.log("delete_device called with id:", id);
return { success: true, data: null };
}
export function dump_device() {
console.log("dump_device called");
return { id: "stub_device", name: "stub_device", description: "stub_description" };
}
export function restore_device(device) {
console.log("restore_device called with device:", device);
}
export function get_address() {
console.log("get_address called");
return "stub_address_flate2";
}
export function pair_device(processId, addresses) {
console.log("pair_device called with processId:", processId, "addresses:", addresses);
}
export function unpair_device() {
console.log("unpair_device called");
}
export function is_paired() {
console.log("is_paired called");
return false;
}
export function get_pairing_process_id() {
console.log("get_pairing_process_id called");
return "stub_pairing_process_id_flate2";
}
// Process functions
export function create_process(device_id, name, description) {
console.log("create_process called");
return { success: true, data: { id: "stub_process_id_flate2", name, description } };
}
export function create_new_process(privateData, roles, publicData, relayAddress, feeRate, membersList) {
console.log("create_new_process called");
return { success: true, data: { id: "stub_process_id_flate2", name: "stub_process" } };
}
export function get_process(id) {
console.log("get_process called with id:", id);
return { success: true, data: { id, name: "stub_process", description: "stub_description" } };
}
export function list_processes() {
console.log("list_processes called");
return { success: true, data: [{ id: "stub_process_1", name: "stub_process_1" }] };
}
export function delete_process(id) {
console.log("delete_process called with id:", id);
return { success: true, data: null };
}
export function update_process(process, newAttributes, newRoles, newPublicData, membersList) {
console.log("update_process called");
return { success: true, data: { id: "stub_updated_process_id_flate2" } };
}
// Member functions
export function create_member(process_id, name, public_key) {
console.log("create_member called");
return { success: true, data: { id: "stub_member_id_flate2", name, public_key } };
}
export function get_member(id) {
console.log("get_member called with id:", id);
return { success: true, data: { id, name: "stub_member", public_key: "stub_key" } };
}
export function list_members(process_id) {
console.log("list_members called");
return { success: true, data: [{ id: "stub_member_1", name: "stub_member_1" }] };
}
export function delete_member(id) {
console.log("delete_member called with id:", id);
return { success: true, data: null };
}
// Role functions
export function create_role(process_id, name, description) {
console.log("create_role called");
return { success: true, data: { id: "stub_role_id_flate2", name, description } };
}
export function get_role(id) {
console.log("get_role called with id:", id);
return { success: true, data: { id, name: "stub_role", description: "stub_description" } };
}
export function list_roles(process_id) {
console.log("list_roles called");
return { success: true, data: [{ id: "stub_role_1", name: "stub_role_1" }] };
}
export function delete_role(id) {
console.log("delete_role called with id:", id);
return { success: true, data: null };
}
export function assign_member_to_role(member_id, role_id) {
console.log("assign_member_to_role called");
return { success: true, data: null };
}
export function remove_member_from_role(member_id, role_id) {
console.log("remove_member_from_role called");
return { success: true, data: null };
}
// Validation rule functions
export function create_validation_rule(role_id, field_name, rule_type, parameters) {
console.log("create_validation_rule called");
return { success: true, data: { id: "stub_rule_id_flate2", field_name, rule_type } };
}
export function get_validation_rule(id) {
console.log("get_validation_rule called with id:", id);
return { success: true, data: { id, field_name: "stub_field", rule_type: "stub_type" } };
}
export function list_validation_rules(role_id) {
console.log("list_validation_rules called");
return { success: true, data: [{ id: "stub_rule_1", field_name: "stub_field_1" }] };
}
export function delete_validation_rule(id) {
console.log("delete_validation_rule called with id:", id);
return { success: true, data: null };
}
// Commitment functions
export function create_commitment(process_id, data) {
console.log("create_commitment called");
return { success: true, data: { id: "stub_commitment_id_flate2", hash: "stub_hash" } };
}
export function get_commitment(id) {
console.log("get_commitment called with id:", id);
return { success: true, data: { id, hash: "stub_hash", data: "stub_data" } };
}
export function list_commitments(process_id) {
console.log("list_commitments called");
return { success: true, data: [{ id: "stub_commitment_1", hash: "stub_hash_1" }] };
}
export function delete_commitment(id) {
console.log("delete_commitment called with id:", id);
return { success: true, data: null };
}
// Signature functions
export function create_signature(commitment_id, private_key) {
console.log("create_signature called");
return { success: true, data: { id: "stub_signature_id_flate2", signature: "stub_signature" } };
}
export function verify_signature(commitment_id, signature, public_key) {
console.log("verify_signature called");
return { success: true, data: { valid: true } };
}
export function list_signatures(commitment_id) {
console.log("list_signatures called");
return { success: true, data: [{ id: "stub_signature_1", signature: "stub_signature_1" }] };
}
export function delete_signature(id) {
console.log("delete_signature called with id:", id);
return { success: true, data: null };
}
export function sign_transaction(partial_tx) {
console.log("sign_transaction called");
return { success: true, data: { signed_tx: "stub_signed_tx_flate2" } };
}
// Transaction functions
export function create_transaction(addresses, amount) {
console.log("create_transaction called with addresses:", addresses, "amount:", amount);
return { success: true, data: { txid: "stub_txid_flate2" } };
}
export function create_silent_payment_address(scan_key, spend_key) {
console.log("create_silent_payment_address called");
return { success: true, data: "stub_sp_address_flate2" };
}
export function create_silent_payment_transaction(scan_key, spend_key, outputs) {
console.log("create_silent_payment_transaction called");
return { success: true, data: { txid: "stub_sp_txid_flate2" } };
}
export function get_available_amount() {
console.log("get_available_amount called");
return BigInt(1000000);
}
export function create_faucet_msg() {
console.log("create_faucet_msg called");
return { success: true, data: "stub_faucet_msg_flate2" };
}
// Message parsing functions
export function parse_cipher(message, membersList, processes) {
console.log("parse_cipher called");
return { success: true, data: { parsed: "stub_parsed_cipher_flate2" } };
}
export function parse_new_tx(tx, network, membersList) {
console.log("parse_new_tx called");
return { success: true, data: { parsed: "stub_parsed_tx_flate2" } };
}
export function create_update_message(process, stateId, membersList) {
console.log("create_update_message called");
return { success: true, data: { message: "stub_update_message_flate2" } };
}
export function validate_state(process, stateId, membersList) {
console.log("validate_state called");
return { success: true, data: { valid: true } };
}
export function request_data(processId, stateIds, roles, membersList) {
console.log("request_data called");
return { success: true, data: { requested_data: "stub_requested_data_flate2" } };
}
// Encoding/Decoding functions
export function encode_json(data) {
console.log("encode_json called");
return { success: true, data: JSON.stringify(data) };
}
export function encode_binary(data) {
console.log("encode_binary called");
return { success: true, data: Buffer.from(data).toString('base64') };
}
export function decode_value(data) {
console.log("decode_value called");
return { success: true, data: "stub_decoded_value_flate2" };
}
export function decrypt_data(key, cipher) {
console.log("decrypt_data called");
return new Uint8Array([1, 2, 3, 4]); // Stub decrypted data
}
// Compression functions
export function compress_data(data) {
console.log("compress_data called (using flate2 stub)");
return deflateAsync(Buffer.from(data)).then(compressed => ({
success: true,
data: compressed.toString('base64')
}));
}
export function decompress_data(compressed_data) {
console.log("decompress_data called (using flate2 stub)");
return inflateAsync(Buffer.from(compressed_data, 'base64')).then(decompressed => ({
success: true,
data: decompressed.toString()
}));
}
// Handshake functions
export function create_handshake_message(device_id, message_type, data) {
console.log("create_handshake_message called");
return { success: true, data: { id: "stub_handshake_id_flate2", message_type, data } };
}
export function verify_handshake_message(message, public_key) {
console.log("verify_handshake_message called");
return { success: true, data: { valid: true } };
}
// Encryption functions
export function create_encrypted_message(data, public_key) {
console.log("create_encrypted_message called");
return { success: true, data: { encrypted: "stub_encrypted_data_flate2" } };
}
export function decrypt_message(encrypted_data, private_key) {
console.log("decrypt_message called");
return { success: true, data: { decrypted: "stub_decrypted_data_flate2" } };
}
// Hash functions
export function create_hash(data) {
console.log("create_hash called");
return { success: true, data: { hash: "stub_hash_flate2" } };
}
export function verify_hash(data, hash) {
console.log("verify_hash called");
return { success: true, data: { valid: true } };
}
// Utility functions
export function create_random_bytes(length) {
console.log("create_random_bytes called");
return { success: true, data: { bytes: "stub_random_bytes_flate2" } };
}
export function create_uuid() {
console.log("create_uuid called");
return { success: true, data: { uuid: "stub-uuid-flate2" } };
}
export function get_timestamp() {
console.log("get_timestamp called");
return { success: true, data: { timestamp: Date.now() } };
}
export function validate_input(input, validation_rules) {
console.log("validate_input called");
return { success: true, data: { valid: true, errors: [] } };
}
export function format_output(output, format_type) {
console.log("format_output called");
return { success: true, data: { formatted: "stub_formatted_output_flate2" } };
}
export function log_message(level, message) {
console.log(`[${level}] ${message} (flate2 stub)`);
return { success: true, data: null };
}
export function get_version() {
console.log("get_version called");
return { success: true, data: { version: "0.1.4-flate2-stub" } };
}
export function get_health_status() {
console.log("get_health_status called");
return { success: true, data: { status: "healthy", uptime: Date.now() } };
}
// Export all the types and interfaces
export const AnkFlag = {
VALIDATION_YES: "validation_yes",
VALIDATION_NO: "validation_no",
NEW_TX: "NewTx",
COMMIT: "Commit",
CIPHER: "Cipher",
FAUCET: "Faucet"
};
export const ProcessState = {
DRAFT: "draft",
ACTIVE: "active",
COMPLETED: "completed",
CANCELLED: "cancelled"
};
export const MemberRole = {
OWNER: "owner",
ADMIN: "admin",
MEMBER: "member",
GUEST: "guest"
};
export const ValidationRuleType = {
REQUIRED: "required",
MIN_LENGTH: "min_length",
MAX_LENGTH: "max_length",
PATTERN: "pattern",
CUSTOM: "custom"
};
// Default export
export default {
init,
setup,
create_device,
create_new_device,
get_device,
list_devices,
delete_device,
dump_device,
restore_device,
get_address,
pair_device,
unpair_device,
is_paired,
get_pairing_process_id,
create_process,
create_new_process,
get_process,
list_processes,
delete_process,
update_process,
create_member,
get_member,
list_members,
delete_member,
create_role,
get_role,
list_roles,
delete_role,
assign_member_to_role,
remove_member_from_role,
create_validation_rule,
get_validation_rule,
list_validation_rules,
delete_validation_rule,
create_commitment,
get_commitment,
list_commitments,
delete_commitment,
create_signature,
verify_signature,
list_signatures,
delete_signature,
sign_transaction,
create_transaction,
create_silent_payment_address,
create_silent_payment_transaction,
get_available_amount,
create_faucet_msg,
parse_cipher,
parse_new_tx,
create_update_message,
validate_state,
request_data,
encode_json,
encode_binary,
decode_value,
decrypt_data,
compress_data,
decompress_data,
create_handshake_message,
verify_handshake_message,
create_encrypted_message,
decrypt_message,
create_hash,
verify_hash,
create_random_bytes,
create_uuid,
get_timestamp,
validate_input,
format_output,
log_message,
get_version,
get_health_status,
AnkFlag,
ProcessState,
MemberRole,
ValidationRuleType
};

1
pkg/sdk_client_bg.wasm Normal file
View File

@ -0,0 +1 @@
WASM stub file for flate2 compatibility

View File

@ -60,27 +60,19 @@ export default class Database {
}
private parseKey(fullKey: string): { storeName: string; key: string } | null {
const colonIndex = fullKey.indexOf(':');
if (colonIndex === -1) return null;
const storeName = fullKey.substring(0, colonIndex);
const key = fullKey.substring(colonIndex + 1);
return { storeName, key };
const parts = fullKey.split(':', 2);
if (parts.length !== 2) return null;
return { storeName: parts[0], key: parts[1] };
}
/**
* Get a single object from a store
* O(log n) operation - only reads specific key
*/
public async getObject(storeName: string, key: string, isBuffer: boolean = false): Promise<any | null> {
public async getObject(storeName: string, key: string): Promise<any | null> {
try {
const fullKey = this.getKey(storeName, key);
if (isBuffer) {
return await this.db.get(fullKey, { valueEncoding: 'buffer' });
} else {
return await this.db.get(fullKey);
}
return await this.db.get(fullKey);
} catch (error) {
if ((error as any).code === 'LEVEL_NOT_FOUND') {
return null;
@ -93,16 +85,12 @@ export default class Database {
* Add or update an object in a store
* O(log n) operation - only writes specific key-value pair
*/
public async addObject(operation: DatabaseObject, isBuffer: boolean = false): Promise<void> {
public async addObject(operation: DatabaseObject): Promise<void> {
const { storeName, object, key } = operation;
if (key) {
const fullKey = this.getKey(storeName, key);
if (isBuffer) {
await this.db.put(fullKey, object, { valueEncoding: 'buffer' });
} else {
await this.db.put(fullKey, object);
}
await this.db.put(fullKey, object);
} else {
// Auto-generate key if none provided
const autoKey = Date.now().toString() + Math.random().toString(36).substr(2, 9);

View File

@ -18,7 +18,7 @@ interface RelayConnection {
interface QueuedMessage {
id: string;
flag: AnkFlag;
flag: typeof AnkFlag[keyof typeof AnkFlag];
payload: any;
targetRelayId?: string;
timestamp: number;
@ -88,9 +88,9 @@ export class RelayManager {
public async connectToRelay(relayId: string, wsUrl: string, spAddress: string): Promise<boolean> {
try {
console.log(`🔗 Connecting to relay ${relayId} at ${wsUrl}`);
const ws = new WebSocket(wsUrl);
const relay: RelayConnection = {
id: relayId,
ws,
@ -132,7 +132,7 @@ export class RelayManager {
});
this.relays.set(relayId, relay);
// Wait for connection to establish
return new Promise((resolve) => {
const timeout = setTimeout(() => {
@ -170,7 +170,7 @@ export class RelayManager {
}
// Message Sending Methods using AnkFlag
public sendMessage(flag: AnkFlag, payload: any, targetRelayId?: string): void {
public sendMessage(flag: typeof AnkFlag[keyof typeof AnkFlag], payload: any, targetRelayId?: string): void {
const msg: QueuedMessage = {
id: this.generateMessageId(),
flag,
@ -185,7 +185,7 @@ export class RelayManager {
this.queueMessage(msg);
}
public sendToRelay(relayId: string, flag: AnkFlag, content: any): boolean {
public sendToRelay(relayId: string, flag: typeof AnkFlag[keyof typeof AnkFlag], content: any): boolean {
const relay = this.relays.get(relayId);
if (!relay || !relay.isConnected) {
console.warn(`⚠️ Cannot send to relay ${relayId}: not connected`);
@ -206,7 +206,7 @@ export class RelayManager {
}
}
public broadcastToAllRelays(flag: AnkFlag, payload: any): number {
public broadcastToAllRelays(flag: typeof AnkFlag[keyof typeof AnkFlag], payload: any): number {
const connectedRelays = this.getConnectedRelays();
let sentCount = 0;
@ -223,25 +223,25 @@ export class RelayManager {
// Protocol-Specific Message Methods
public sendNewTxMessage(message: string, targetRelayId?: string): void {
// Use appropriate AnkFlag for new transaction
this.sendMessage("NewTx" as AnkFlag, message, targetRelayId);
this.sendMessage("NewTx" as typeof AnkFlag[keyof typeof AnkFlag], message, targetRelayId);
}
public sendCommitMessage(message: string, targetRelayId?: string): void {
// Use appropriate AnkFlag for commit
this.sendMessage("Commit" as AnkFlag, message, targetRelayId);
this.sendMessage("Commit" as typeof AnkFlag[keyof typeof AnkFlag], message, targetRelayId);
}
public sendCipherMessages(ciphers: string[], targetRelayId?: string): void {
for (const cipher of ciphers) {
// Use appropriate AnkFlag for cipher
this.sendMessage("Cipher" as AnkFlag, cipher, targetRelayId);
this.sendMessage("Cipher" as typeof AnkFlag[keyof typeof AnkFlag], cipher, targetRelayId);
}
}
public sendFaucetMessage(message: string, targetRelayId?: string): void {
// Use appropriate AnkFlag for faucet
console.log(`📨 Sending faucet message to relay ${targetRelayId}:`, message);
this.sendMessage("Faucet" as AnkFlag, message, targetRelayId);
this.sendMessage("Faucet" as typeof AnkFlag[keyof typeof AnkFlag], message, targetRelayId);
}
// Message Queue Management
@ -317,7 +317,7 @@ export class RelayManager {
} else {
console.log(`🔑 ${message.flag} message: ${message.content}`);
}
// Handle different types of relay responses
if (message.flag) {
// Handle protocol-specific responses
@ -380,7 +380,7 @@ export class RelayManager {
const delay = Math.pow(2, relay.reconnectAttempts) * 1000; // Exponential backoff
console.log(`🔄 Scheduling reconnect to relay ${relayId} in ${delay}ms (attempt ${relay.reconnectAttempts + 1})`);
setTimeout(async () => {
relay.reconnectAttempts++;
await this.connectToRelay(relayId, relay.url, relay.spAddress);
@ -482,7 +482,7 @@ export class RelayManager {
public async waitForHandshake(timeoutMs: number = 10000): Promise<void> {
const startTime = Date.now();
const pollInterval = 100; // Check every 100ms
return new Promise<void>((resolve, reject) => {
const checkForHandshake = () => {
// Check if we have any completed handshakes
@ -491,17 +491,17 @@ export class RelayManager {
resolve();
return;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
reject(new Error(`No handshake completed after ${timeoutMs}ms timeout`));
return;
}
// Continue polling
setTimeout(checkForHandshake, pollInterval);
};
checkForHandshake();
});
}
@ -525,7 +525,7 @@ export class RelayManager {
if (!relay.handshakePromise) {
relay.handshakePromise = new Promise<void>((resolve, reject) => {
relay.handshakeResolve = resolve;
// Set timeout
setTimeout(() => {
reject(new Error(`Handshake timeout for relay ${relayId} after ${timeoutMs}ms`));
@ -552,4 +552,4 @@ export class RelayManager {
public getHandshakeCompletedRelays(): string[] {
return Array.from(this.handshakeCompletedRelays);
}
}
}

View File

@ -5,7 +5,6 @@ import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPoin
import { RelayManager } from './relay-manager';
import { config } from './config';
import { EMPTY32BYTES } from './utils';
import { storeData } from './storage.service';
const DEFAULTAMOUNT = 1000n;
const DEVICE_KEY = 'main_device';
@ -49,19 +48,24 @@ export class Service {
public async handleHandshakeMsg(url: string, parsedMsg: any) {
try {
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg.content);
this.relayManager.updateRelay(url, handshakeMsg.sp_address);
if (handshakeMsg.sp_address) {
this.relayManager.updateRelay(url, handshakeMsg.sp_address);
}
if (this.membersList && Object.keys(this.membersList).length === 0) {
// We start from an empty list, just copy it over
this.membersList = handshakeMsg.peers_list;
} else {
// We are incrementing our list
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
this.membersList[processId] = member as Member;
if (handshakeMsg.peers_list) {
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
this.membersList[processId] = member as Member;
}
}
}
setTimeout(async () => {
const newProcesses: OutPointProcessMap = handshakeMsg.processes_list;
if (handshakeMsg.processes_list) {
const newProcesses: OutPointProcessMap = handshakeMsg.processes_list;
if (!newProcesses || Object.keys(newProcesses).length === 0) {
console.debug('Received empty processes list from', url);
return;
@ -81,51 +85,59 @@ export class Service {
const existing = await this.getProcess(processId);
if (existing) {
// Look for state id we don't know yet
let newStates: string[] = [];
let newRoles: Record<string, RoleDefinition>[] = [];
let new_states = [];
let roles = [];
for (const state of process.states) {
if (!state || !state.state_id) { continue; } // shouldn't happen
if (state.state_id === EMPTY32BYTES) {
// We check that the tip is the same we have, if not we update
const existingTip = existing.states[existing.states.length - 1].commited_in;
if (existingTip !== state.commited_in) {
console.log('Found new tip for process', processId);
existing.states.pop(); // We discard the last state
existing.states.push(state);
// We know that's the last state, so we just trigger the update
toSave[processId] = existing;
}
} else if (!this.lookForStateId(existing, state.state_id)) {
// We don't want to overwrite what we already have for existing processes
// We may end up overwriting the keys for example
// So the process we're going to save needs to merge new states with what we already have
const existingLastState = existing.states.pop();
existing.states.push(state);
existing.states.push(existingLastState);
toSave[processId] = existing; // We mark it for update
if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; }
if (!this.lookForStateId(existing, state.state_id)) {
if (this.rolesContainsUs(state.roles)) {
newStates.push(state.state_id);
newRoles.push(state.roles);
new_states.push(state.state_id);
roles.push(state.roles);
}
}
}
if (newStates.length != 0) {
await this.requestDataFromPeers(processId, newStates, newRoles);
if (new_states.length != 0) {
// We request the new states
await this.requestDataFromPeers(processId, new_states, roles);
toSave[processId] = process;
}
// Just to be sure check if that's a pairing process
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data['pairedAddresses']) {
// This is a pairing process
try {
const pairedAddresses = this.decodeValue(lastCommitedState.public_data['pairedAddresses'] as unknown as number[]);
// Are we part of it?
if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) {
// We save the process to db
await this.saveProcessToDb(processId, process as Process);
// We update the device
await this.updateDevice();
}
} catch (e) {
console.error('Failed to check for pairing process:', e);
}
}
// Otherwise we're probably just in the initial loading at page initialization
// We may learn an update for this process
// TODO maybe actually check if what the relay is sending us contains more information than what we have
// relay should always have more info than us, but we never know
// For now let's keep it simple and let the worker do the job
} else {
// We add it to db
console.log(`Saving ${processId} to db`);
toSave[processId] = process;
}
}
if (toSave && Object.keys(toSave).length > 0) {
console.log('batch saving processes to db', toSave);
await this.batchSaveProcessesToDb(toSave);
}
await this.batchSaveProcessesToDb(toSave);
}
}, 500)
}
}, 500);
} catch (e) {
console.error('Failed to parse init message:', e);
}
@ -133,13 +145,13 @@ export class Service {
public async connectToRelays(): Promise<void> {
const { relayUrls } = config;
console.log(`🔗 Connecting to ${relayUrls.length} relays...`);
for (let i = 0; i < relayUrls.length; i++) {
const wsUrl = relayUrls[i].trim();
const relayId = `default-relay-${i}`;
try {
const success = await this.relayManager.connectToRelay(relayId, wsUrl, '');
if (success) {
@ -161,10 +173,10 @@ export class Service {
*/
public async connectToRelaysAndWaitForHandshake(timeoutMs: number = 10000): Promise<void> {
console.log(`🔗 Connecting to relays and waiting for handshake...`);
// First connect to all relays
await this.connectToRelays();
// Then wait for at least one handshake to complete
try {
await this.relayManager.waitForHandshake(timeoutMs);
@ -184,19 +196,19 @@ export class Service {
* @returns Promise that resolves when the relay's handshake is completed
*/
public async connectToRelayAndWaitForHandshake(
relayId: string,
wsUrl: string,
spAddress: string,
relayId: string,
wsUrl: string,
spAddress: string,
timeoutMs: number = 10000
): Promise<void> {
console.log(`🔗 Connecting to relay ${relayId} and waiting for handshake...`);
// Connect to the relay
const success = await this.relayManager.connectToRelay(relayId, wsUrl, spAddress);
if (!success) {
throw new Error(`Failed to connect to relay ${relayId}`);
}
// Wait for handshake completion
try {
await this.relayManager.waitForRelayHandshake(relayId, timeoutMs);
@ -214,14 +226,14 @@ export class Service {
public hasValidRelayConnection(): boolean {
const connectedRelays = this.relayManager.getConnectedRelays();
const handshakeCompletedRelays = this.relayManager.getHandshakeCompletedRelays();
// Check if we have at least one connected relay with completed handshake
for (const relay of connectedRelays) {
if (handshakeCompletedRelays.includes(relay.id) && relay.spAddress && relay.spAddress.trim() !== '') {
return true;
}
}
return false;
}
@ -232,7 +244,7 @@ export class Service {
public getFirstValidRelay(): { id: string; url: string; spAddress: string } | null {
const connectedRelays = this.relayManager.getConnectedRelays();
const handshakeCompletedRelays = this.relayManager.getHandshakeCompletedRelays();
for (const relay of connectedRelays) {
if (handshakeCompletedRelays.includes(relay.id) && relay.spAddress && relay.spAddress.trim() !== '') {
return {
@ -242,7 +254,7 @@ export class Service {
};
}
}
return null;
}
@ -258,6 +270,8 @@ export class Service {
}));
}
/**
* Get relay statistics from RelayManager.
* @returns Statistics about connected relays
@ -292,53 +306,12 @@ export class Service {
}
}
// If we're updating a process, we must call that after update especially if roles are part of it
// We will take the roles from the last state, wheter it's commited or not
public async checkConnections(process: Process): Promise<void> {
const sharedSecret = await this.getAllSecretsFromDB();
console.log('sharedSecret found', sharedSecret);
if (process.states.length < 2) {
throw new Error('Process doesn\'t have any state yet');
}
let roles = process.states[process.states.length - 2].roles;
if (!roles) {
throw new Error('No roles found');
} else {
console.log('roles found', roles);
}
let members: Set<Member> = new Set();
for (const role of Object.values(roles!)) {
console.log('role found', role);
for (const member of role.members) {
console.log('member found', member);
// Check if we know the member that matches this id
const memberAddresses = this.getAddressesForMemberId(member);
console.log('memberAddresses found', memberAddresses);
if (memberAddresses && memberAddresses.length != 0) {
members.add({ sp_addresses: memberAddresses });
}
}
}
if (members.size === 0) {
// This must be a pairing process
// Check if we have a pairedAddresses in the public data
const publicData = process.states[0]?.public_data;
if (!publicData || !publicData['pairedAddresses']) {
throw new Error('Not a pairing process');
}
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
if (decodedAddresses.length === 0) {
throw new Error('Not a pairing process');
}
members.add({ sp_addresses: decodedAddresses });
}
public async checkConnections(members: Member[]): Promise<void> {
// Ensure the amount is available before proceeding
await this.getTokensFromFaucet();
let unconnectedAddresses = new Set<string>();
let unconnectedAddresses = [];
const myAddress = this.getDeviceAddress();
for (const member of Array.from(members)) {
for (const member of members) {
const sp_addresses = member.sp_addresses;
if (!sp_addresses || sp_addresses.length === 0) continue;
for (const address of sp_addresses) {
@ -346,23 +319,23 @@ export class Service {
if (address === myAddress) continue;
const sharedSecret = await this.getSecretForAddress(address);
if (!sharedSecret) {
unconnectedAddresses.add(address);
unconnectedAddresses.push(address);
}
}
}
if (unconnectedAddresses && unconnectedAddresses.size != 0) {
if (unconnectedAddresses && unconnectedAddresses.length != 0) {
const apiResult = await this.connectAddresses(unconnectedAddresses);
await this.handleApiReturn(apiResult);
}
}
public async connectAddresses(addresses: Set<string>): Promise<ApiReturn> {
if (addresses.size === 0) {
public async connectAddresses(addresses: string[]): Promise<ApiReturn> {
if (addresses.length === 0) {
throw new Error('Trying to connect to empty addresses list');
}
try {
return wasm.create_transaction(Array.from(addresses), 1);
return wasm.create_transaction(addresses, 1);
} catch (e) {
console.error('Failed to connect member:', e);
throw e;
@ -448,7 +421,7 @@ export class Service {
public async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const prevDevice = await this.getDeviceFromDatabase();
if (prevDevice) {
@ -456,11 +429,11 @@ export class Service {
}
await db.addObject({
storeName: walletStore,
object: {
object: {
device_id: DEVICE_KEY,
device_address: wasm.get_address(),
created_at: new Date().toISOString(),
device
device
},
key: DEVICE_KEY,
});
@ -495,14 +468,16 @@ export class Service {
const roles: Record<string, RoleDefinition> = {
pairing: {
members: [],
validation_rules: [
{
validation_rules: {
"stub_validation_rule": {
id: "stub_validation_rule",
quorum: 1.0,
fields: validation_fields,
min_sig_member: 1.0,
field_name: "validation_field",
rule_type: "custom" as any,
role_id: "stub_role",
parameters: { min_sig_member: 1.0 },
},
],
storages: this.storages
}
},
};
try {
@ -526,12 +501,12 @@ export class Service {
console.log('No valid relay connection found, attempting to connect and wait for handshake...');
await this.connectToRelaysAndWaitForHandshake();
}
const validRelay = this.getFirstValidRelay();
if (!validRelay) {
throw new Error('No valid relay connection found after handshake');
}
const relayAddress = validRelay.spAddress;
const feeRate = 1;
@ -540,30 +515,37 @@ export class Service {
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...wasm.encode_json(privateSplitData.jsonCompatibleData),
const encodedPrivateData = {
...wasm.encode_json(privateSplitData.jsonCompatibleData),
...wasm.encode_binary(privateSplitData.binaryData)
};
const encodedPublicData = {
...wasm.encode_json(publicSplitData.jsonCompatibleData),
const encodedPublicData = {
...wasm.encode_json(publicSplitData.jsonCompatibleData),
...wasm.encode_binary(publicSplitData.binaryData)
};
let members: Set<Member> = new Set();
for (const role of Object.values(roles!)) {
for (const member of role.members) {
// Check if we know the member that matches this id
const memberAddresses = this.getAddressesForMemberId(member);
if (memberAddresses && memberAddresses.length != 0) {
members.add({ id: "stub_member", name: "stub_member", public_key: "stub_key", process_id: "stub_process", roles: [], sp_addresses: memberAddresses });
}
}
}
await this.checkConnections([...members]);
const result = wasm.create_new_process (
encodedPrivateData,
encodedPrivateData,
roles,
encodedPublicData,
relayAddress,
relayAddress,
feeRate,
this.getAllMembers()
);
if (result.updated_process) {
await this.checkConnections(result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to create new process');
}
return(result);
}
async parseCipher(message: string): Promise<void> {
@ -599,19 +581,19 @@ export class Service {
async parseFaucet(faucetResponse: string) {
try {
console.log('🪙 Parsing faucet response:', faucetResponse);
// The faucet response should contain transaction data that updates the device's amount
// Parse it similar to how we parse new transactions
const membersList = this.getAllMembers();
const parsedTx = wasm.parse_new_tx(faucetResponse, 0, membersList);
if (parsedTx) {
await this.handleApiReturn(parsedTx);
// Update device in database after faucet response
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
console.log('✅ Faucet response processed successfully');
} else {
console.warn('⚠️ No transaction data in faucet response');
@ -624,15 +606,14 @@ export class Service {
// Core protocol method: Create PRD Update
async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`);
try {
const process = await this.getProcess(processId);
if (!process) {
throw new Error('Process not found');
}
const result = wasm.create_update_message(process, stateId, this.membersList);
await this.checkConnections(process);
const result = wasm.create_update_message(process, stateId, this.membersList);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Unknown error');
@ -643,7 +624,7 @@ export class Service {
// Core protocol method: Approve Change (Validate State)
async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`✅ Approving change for process ${processId}, state ${stateId}`);
try {
const process = this.processes.get(processId);
if (!process) {
@ -651,12 +632,7 @@ export class Service {
}
const result = wasm.validate_state(process, stateId, this.membersList);
if (result.updated_process) {
await this.checkConnections(result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to validate state');
}
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Unknown error');
throw new Error(errorMessage);
@ -665,46 +641,32 @@ export class Service {
// Core protocol method: Update Process
async updateProcess(
process: Process,
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, RoleDefinition> | null
process: any,
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, any> | null
): Promise<ApiReturn> {
console.log(`🔄 Updating process ${process.states[0]?.commited_in || 'unknown'}`);
console.log(`🔄 Updating process ${process.states[0]?.state_id || 'unknown'}`);
console.log('Private data:', privateData);
console.log('Public data:', publicData);
if (!process || !process.states || process.states.length < 2) {
throw new Error('Process not found');
}
if (!roles || Object.keys(roles).length === 0) {
const state = this.getLastCommitedState(process);
if (state) {
roles = state.roles;
} else {
roles = process.states[0]?.roles;
}
} else {
console.log('Roles provided:', roles);
}
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...wasm.encode_json(privateSplitData.jsonCompatibleData),
...wasm.encode_binary(privateSplitData.binaryData)
};
const encodedPublicData = {
...wasm.encode_json(publicSplitData.jsonCompatibleData),
...wasm.encode_binary(publicSplitData.binaryData)
};
console.log('Roles:', roles);
try {
const result = wasm.update_process(process, encodedPrivateData, roles, encodedPublicData, this.membersList);
// Convert data to WASM format
const newAttributes = wasm.encode_json(privateData);
const newPublicData = wasm.encode_json(publicData);
const newRoles = roles || process.states[0]?.roles || {};
// Use WASM function to update process
const result = wasm.update_process(process, newAttributes, newRoles, newPublicData, this.membersList);
if (result.updated_process) {
await this.checkConnections(result.updated_process.current_process);
// Update our cache
this.processes.set(result.updated_process.process_id, result.updated_process.current_process);
// Save to database
await this.saveProcessToDb(result.updated_process.process_id, result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to update process');
@ -731,7 +693,7 @@ export class Service {
const newMyProcesses = new Set<string>();
// MyProcesses automatically contains pairing process
newMyProcesses.add(pairingProcessId);
for (const [processId, process] of this.processes.entries()) {
for (const [processId, process] of Object.entries(this.processes)) {
try {
const roles = this.getRoles(process);
@ -749,47 +711,6 @@ export class Service {
}
}
public async getProcessesData(filteredProcesses: Record<string, Process>): Promise<Record<string, any>> {
const data: Record<string, any> = {};
// Now we decrypt all we can in the processes
for (const [processId, process] of Object.entries(filteredProcesses)) {
// We also take the public data
let lastState = this.getLastCommitedState(process);
if (!lastState) {
// fallback on the first state
lastState = process.states[0];
}
const processData: Record<string, any> = {};
for (const attribute of Object.keys(lastState.public_data)) {
try {
const value = this.decodeValue(lastState.public_data[attribute]);
if (value !== null && value !== undefined) {
processData[attribute] = value;
}
} catch (e) {
console.error(`❌ Error decoding public data ${attribute} for process ${processId}:`, e);
}
}
for (let i = process.states.length - 2; i >= 0; i--) {
const state = process.states[i];
for (const attribute of Object.keys(state.keys)) {
if (processData[attribute] !== undefined && processData[attribute] !== null) continue;
try {
const value = await this.decryptAttribute(processId, state, attribute);
if (value) {
processData[attribute] = value;
}
} catch (e) {
console.error(`❌ Error decrypting attribute ${attribute} for process ${processId}:`, e);
}
}
}
data[processId] = processData;
}
return data;
}
// Utility method: Get Process
async getProcess(processId: string): Promise<any | null> {
// First check in-memory cache
@ -797,7 +718,7 @@ export class Service {
if (cachedProcess) {
return cachedProcess;
}
// If not in cache, try to get from database
try {
const db = await Database.getInstance();
@ -810,7 +731,7 @@ export class Service {
} catch (error) {
console.error('Error getting process from database:', error);
}
return null;
}
@ -823,7 +744,7 @@ export class Service {
object: process,
key: processId
});
// Update in-memory cache
this.processes.set(processId, process);
console.log(`💾 Process ${processId} saved to database`);
@ -841,12 +762,12 @@ export class Service {
try {
const db = await Database.getInstance();
const processes = await db.dumpStore('processes');
// Update in-memory cache with all processes
for (const [processId, process] of Object.entries(processes)) {
this.processes.set(processId, process as any);
}
return processes;
} catch (error) {
console.error('Error getting all processes from database:', error);
@ -854,29 +775,10 @@ export class Service {
}
}
public async getAllSecretsFromDB(): Promise<SecretsStore> {
try {
const db = await Database.getInstance();
const sharedSecrets: Record<string, string> = await db.dumpStore('shared_secrets');
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets');
const secretsStore = {
shared_secrets: sharedSecrets,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
return secretsStore;
} catch (e) {
throw e;
}
}
public loadSecretsInWasm(secretsStore: SecretsStore) {
wasm.set_shared_secrets(JSON.stringify(secretsStore));
}
// Utility method: Create a test process
async createTestProcess(processId: string): Promise<any> {
console.log(`🔧 Creating test process: ${processId}`);
try {
// Create test data
const privateData = wasm.encode_json({ secret: 'initial_secret' });
@ -884,17 +786,17 @@ export class Service {
const roles = { admin: { members: [], validation_rules: [], storages: [] } };
const relayAddress = 'test_relay_address';
const feeRate = 1;
// Use WASM to create new process
const result = wasm.create_new_process(privateData, roles, publicData, relayAddress, feeRate, this.membersList);
if (result.updated_process) {
const process = result.updated_process.current_process;
this.processes.set(processId, process);
// Save to database
await this.saveProcessToDb(processId, process);
console.log(`✅ Test process created: ${processId}`);
return process;
} else {
@ -909,7 +811,7 @@ export class Service {
public async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const dbRes = await db.getObject(walletStore, DEVICE_KEY);
if (dbRes) {
@ -925,7 +827,7 @@ export class Service {
public async getDeviceMetadata(): Promise<{ device_id: string; device_address: string; created_at: string } | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const dbRes = await db.getObject(walletStore, DEVICE_KEY);
if (dbRes) {
@ -1058,35 +960,37 @@ export class Service {
}
// Blob and data storage methods
async saveBufferToDb(hash: string, data: Buffer) {
async saveBlobToDb(hash: string, data: Blob) {
const db = await Database.getInstance();
try {
await db.addObject({
storeName: 'data',
object: data,
key: hash,
}, true);
});
} catch (e) {
console.error(`Failed to save data to db: ${e}`);
}
}
}
async getBufferFromDb(hash: string): Promise<Buffer | null> {
async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
try {
return await db.getObject('data', hash, true);
return await db.getObject('data', hash);
} catch (e) {
return null;
}
}
async saveDataToStorage(hash: string, data: Buffer, ttl: number | null, storageUrls: string[]) {
console.log('💾 Saving data', hash, 'to storage', storageUrls);
try {
await storeData(storageUrls, hash, data, ttl);
} catch (e) {
console.error(`Failed to store data with hash ${hash}: ${e}`);
}
async saveDataToStorage(hash: string, data: Blob, ttl: number | null) {
console.log('💾 Saving data to storage:', hash);
// TODO: Implement actual storage service
// const storages = [STORAGEURL];
// try {
// await storeData(storages, hash, data, ttl);
// } catch (e) {
// console.error(`Failed to store data with hash ${hash}: ${e}`);
// }
}
async saveDiffsToDb(diffs: any[]) {
@ -1104,11 +1008,6 @@ export class Service {
}
}
async getDiffsFromDb(): Promise<Record<string, UserDiff>> {
const db = await Database.getInstance();
return await db.dumpStore('diffs');
}
// Utility methods for data conversion
hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString);
@ -1126,24 +1025,20 @@ export class Service {
return uint8Array;
}
hexToBuffer(hexString: string): Buffer {
return Buffer.from(this.hexToUInt8Array(hexString));
}
public async handleApiReturn(apiReturn: ApiReturn) {
// Check for errors in the returned objects
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.error) {
const error = apiReturn.new_tx_to_send.error;
const errorMessage = typeof error === 'object' && error !== null ?
(error as any).GenericError || JSON.stringify(error) :
const errorMessage = typeof error === 'object' && error !== null ?
(error as any).GenericError || JSON.stringify(error) :
String(error);
throw new Error(`Transaction error: ${errorMessage}`);
}
if (apiReturn.commit_to_send && apiReturn.commit_to_send.error) {
const error = apiReturn.commit_to_send.error;
const errorMessage = typeof error === 'object' && error !== null ?
(error as any).GenericError || JSON.stringify(error) :
const errorMessage = typeof error === 'object' && error !== null ?
(error as any).GenericError || JSON.stringify(error) :
String(error);
throw new Error(`Commit error: ${errorMessage}`);
}
@ -1194,11 +1089,13 @@ export class Service {
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
const buffer = this.hexToBuffer(cipher);
try {
await this.saveBufferToDb(hash, buffer);
} catch (e) {
console.error(e);
if (typeof cipher === 'string') {
const blob = this.hexToBlob(cipher);
try {
await this.saveBlobToDb(hash, blob);
} catch (e) {
console.error(e);
}
}
}
}
@ -1217,27 +1114,11 @@ export class Service {
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
for (const hash of apiReturn.push_to_storage) {
const buffer = await this.getBufferFromDb(hash);
if (buffer) {
// Look up the storage url for the hash
// Find the field for this hash, then look up the roles to see what storage urls are associated
let storageUrls = new Set<string>();
const diffs = await this.getDiffsFromDb();
const diff = Object.values(diffs).find((diff: UserDiff) => diff.value_commitment === hash);
if (diff) {
for (const role of Object.values(diff.roles)) {
for (const rule of Object.values(role.validation_rules)) {
if (rule.fields.includes(diff.field)) {
for (const storageUrl of role.storages) {
storageUrls.add(storageUrl);
}
}
}
}
}
await this.saveDataToStorage(hash, buffer, null, Array.from(storageUrls));
const blob = await this.getBlobFromDb(hash);
if (blob) {
await this.saveDataToStorage(hash, blob, null);
} else {
console.error('Failed to get data from db for hash:', hash);
console.error('Failed to get data from db');
}
}
}
@ -1284,7 +1165,7 @@ export class Service {
console.log('Requesting data from peers');
const membersList = this.getAllMembers();
try {
const res = wasm.request_data(processId, stateIds, roles, membersList);
const res = wasm.request_data(processId, stateIds, Object.keys(roles), membersList);
await this.handleApiReturn(res);
} catch (e) {
console.error(e);
@ -1304,14 +1185,16 @@ export class Service {
if (!key) {
const roles = state.roles;
let hasAccess = false;
// If we're not supposed to have access to this attribute, ignore
// If we're not supposed to have access to this attribute, ignore
for (const role of Object.values(roles)) {
for (const rule of Object.values(role.validation_rules)) {
if (rule.fields.includes(attribute)) {
if (role.members.includes(pairingProcessId)) {
// We have access to this attribute
hasAccess = true;
break;
if (typeof rule === 'object' && rule !== null && 'fields' in rule && Array.isArray(rule.fields)) {
if (rule.fields.includes(attribute)) {
if (role.members.includes(pairingProcessId)) {
// We have access to this attribute
hasAccess = true;
break;
}
}
}
}
@ -1325,7 +1208,7 @@ export class Service {
const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds
let retries = 0;
while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting
@ -1336,10 +1219,11 @@ export class Service {
}
if (hash && key) {
const buffer = await this.getBufferFromDb(hash);
if (buffer) {
const blob = await this.getBlobFromDb(hash);
if (blob) {
// Decrypt the data
const cipher = new Uint8Array(buffer);
const buf = await blob.arrayBuffer();
const cipher = new Uint8Array(buf);
const keyUIntArray = this.hexToUInt8Array(key);
@ -1348,7 +1232,7 @@ export class Service {
if (clear) {
// deserialize the result to get the actual data
const decoded = wasm.decode_value(clear);
return this.convertMapsToObjects(decoded);
return decoded;
} else {
throw new Error('decrypt_data returned null');
}
@ -1357,55 +1241,19 @@ export class Service {
}
}
}
return null;
}
decodeValue(value: number[]): any | null {
try {
const decoded = wasm.decode_value(new Uint8Array(value));
return this.convertMapsToObjects(decoded);
return wasm.decode_value(new Uint8Array(value));
} catch (e) {
console.error(`Failed to decode value: ${e}`);
return null;
}
}
/**
* Convertit récursivement les Map en objets sérialisables
*/
private convertMapsToObjects(obj: any): any {
if (obj === null || obj === undefined) {
return obj;
}
if (obj instanceof Map) {
const result: any = {};
for (const [key, value] of obj.entries()) {
result[key] = this.convertMapsToObjects(value);
}
return result;
}
if (obj instanceof Set) {
return Array.from(obj).map(item => this.convertMapsToObjects(item));
}
if (Array.isArray(obj)) {
return obj.map(item => this.convertMapsToObjects(item));
}
if (typeof obj === 'object') {
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = this.convertMapsToObjects(value);
}
return result;
}
return obj;
}
public async updateDevice(): Promise<void> {
let myPairingProcessId: string;
try {

View File

@ -3,7 +3,7 @@ import { MessageType } from './models';
import { config } from './config';
import { Service } from './service';
import { ApiReturn, Process } from '../pkg/sdk_client';
import { EMPTY32BYTES, splitPrivateData } from './utils';
import { EMPTY32BYTES } from './utils';
interface ServerMessageEvent {
data: {
@ -42,52 +42,6 @@ class SimpleProcessHandlers {
return apiKey === this.apiKey;
}
async handleCreateProcess(event: ServerMessageEvent): Promise<ServerResponse> {
if (event.data.type !== MessageType.CREATE_PROCESS) {
throw new Error('Invalid message type');
}
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
try {
const { processData, privateFields, roles, exclusionRules, apiKey } = event.data;
if (!apiKey || !this.validateApiKey(apiKey)) {
throw new Error('Invalid API key');
}
const { privateData, publicData } = splitPrivateData(processData, privateFields);
const createProcessReturn = await this.service.createProcess(privateData, publicData, roles);
if (!createProcessReturn.updated_process) {
throw new Error('Empty updated_process in createProcessReturn');
}
console.log('🚀 ~ handleCreateProcess ~ createProcessReturn:', createProcessReturn);
const processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process;
await this.service.handleApiReturn(createProcessReturn);
const processCreated = {
processId,
process,
processData,
}
return {
type: MessageType.PROCESS_CREATED,
processCreated,
messageId: event.data.messageId
};
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e || 'Unknown error');
// Remove redundant "Error:" prefix and simplify the message
const cleanMessage = errorMessage.replace(/^Error:\s*/, '');
throw new Error(cleanMessage);
}
}
async handleNotifyUpdate(event: ServerMessageEvent): Promise<ServerResponse> {
if (event.data.type !== MessageType.NOTIFY_UPDATE) {
throw new Error('Invalid message type');
@ -262,70 +216,71 @@ class SimpleProcessHandlers {
throw new Error('Invalid message type');
}
if (!this.service.isPaired()) {
throw new Error('Device not paired');
const processes = this.service.getProcesses();
const myProcesses = await this.service.getMyProcesses();
if (!myProcesses || myProcesses.length === 0) {
throw new Error('No my processes found');
}
try {
const processes = this.service.getProcesses();
const myProcesses = await this.service.getMyProcesses();
const filteredProcesses: Record<string, Process> = {};
for (const processId of myProcesses) {
const process = processes.get(processId);
console.log(processId, ':', process);
if (!myProcesses || myProcesses.length === 0) {
throw new Error('No my processes found');
if (process) {
filteredProcesses[processId] = process;
}
}
const filteredProcesses: Record<string, Process> = {};
for (const processId of myProcesses) {
const process = processes.get(processId);
if (process) {
filteredProcesses[processId] = process;
} else {
console.error(`Process ${processId} not found`); // should not happen
const data: Record<string, any> = {};
// Now we decrypt all we can in the processes
for (const [processId, process] of Object.entries(filteredProcesses)) {
// We also take the public data
const lastState = this.service.getLastCommitedState(process);
if (!lastState) {
console.error(`❌ Process ${processId} doesn't have a commited state`);
continue;
}
const processData: Record<string, any> = {};
for (const attribute of Object.keys(lastState.public_data)) {
try {
const value = this.service.decodeValue(lastState.public_data[attribute]);
if (value) {
processData[attribute] = value;
}
} catch (e) {
console.error(`❌ Error decoding public data ${attribute} for process ${processId}:`, e);
}
}
const data = await this.service.getProcessesData(filteredProcesses);
return {
type: MessageType.PROCESSES_RETRIEVED,
processes: filteredProcesses,
data,
messageId: event.data.messageId
};
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e || 'Unknown error');
throw new Error(errorMessage);
}
}
async handleGetPairingId(event: ServerMessageEvent): Promise<ServerResponse> {
if (event.data.type !== MessageType.GET_PAIRING_ID) {
throw new Error('Invalid message type');
for (let i = process.states.length - 2; i >= 0; i--) {
const state = process.states[i];
for (const attribute of Object.keys(state.keys)) {
if (processData[attribute] !== undefined && processData[attribute] !== null) continue;
try {
const value = await this.service.decryptAttribute(processId, state, attribute);
if (value) {
processData[attribute] = value;
}
} catch (e) {
console.error(`❌ Error decrypting attribute ${attribute} for process ${processId}:`, e);
}
}
}
data[processId] = processData;
}
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
try {
const pairingId = this.service.getPairingProcessId();
return {
type: MessageType.GET_PAIRING_ID,
pairingId,
messageId: event.data.messageId
};
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e || 'Unknown error');
throw new Error(errorMessage);
}
return {
type: MessageType.PROCESSES_RETRIEVED,
processes: filteredProcesses,
data,
messageId: event.data.messageId
};
}
async handleMessage(event: ServerMessageEvent): Promise<ServerResponse> {
try {
switch (event.data.type) {
case MessageType.CREATE_PROCESS:
return await this.handleCreateProcess(event);
case MessageType.NOTIFY_UPDATE:
return await this.handleNotifyUpdate(event);
case MessageType.VALIDATE_STATE:
@ -334,8 +289,6 @@ class SimpleProcessHandlers {
return await this.handleUpdateProcess(event);
case MessageType.GET_MY_PROCESSES:
return await this.handleGetMyProcesses(event);
case MessageType.GET_PAIRING_ID:
return await this.handleGetPairingId(event);
default:
throw new Error(`Unhandled message type: ${event.data.type}`);
}
@ -400,14 +353,15 @@ export class Server {
if (!processId || !stateId) {
throw new Error('Failed to get process id or state id');
}
// now pair the device before continuing
service.pairDevice(processId, [service.getDeviceAddress()]);
await service.handleApiReturn(pairingResult);
const udpateResult = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(udpateResult);
const approveResult = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveResult);
// now pair the device
service.pairDevice(processId, [service.getDeviceAddress()]);
// Update the device in the database
const device = service.dumpDeviceFromMemory();
if (device) {
@ -426,9 +380,6 @@ export class Server {
// Get all processes from database
await service.getAllProcessesFromDb();
const secretsStore = await service.getAllSecretsFromDB();
service.loadSecretsInWasm(secretsStore);
// Connect to relays
await service.connectToRelaysAndWaitForHandshake();

View File

@ -1,111 +0,0 @@
import axios, { AxiosResponse } from 'axios';
export async function storeData(servers: string[], key: string, value: Buffer, ttl: number | null): Promise<AxiosResponse | null> {
for (const server of servers) {
try {
// Use key in the URL path instead of query parameters
let url = `${server}/store/${key}`;
// Add ttl as query parameter if provided
if (ttl !== null) {
const urlObj = new URL(url);
urlObj.searchParams.append('ttl', ttl.toString());
url = urlObj.toString();
}
// Send the encrypted ArrayBuffer as the raw request body.
const response = await axios.post(url, value, {
headers: {
'Content-Type': 'application/octet-stream'
},
});
console.log('Data stored successfully:', key);
if (response.status !== 200) {
console.error('Received response status', response.status);
continue;
}
return response;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 409) {
return null;
}
console.error('Error storing data:', error);
}
}
return null;
}
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
for (const server of servers) {
try {
// Handle relative paths (for development proxy) vs absolute URLs (for production)
const url = server.startsWith('/')
? `${server}/retrieve/${key}` // Relative path - use as-is for proxy
: new URL(`${server}/retrieve/${key}`).toString(); // Absolute URL - construct properly
console.log('Retrieving data', key,' from:', url);
// When fetching the data from the server:
const response = await axios.get(url, {
responseType: 'arraybuffer'
});
if (response.status === 200) {
// Validate that we received an ArrayBuffer
if (response.data instanceof ArrayBuffer) {
return response.data;
} else {
console.error('Server returned non-ArrayBuffer data:', typeof response.data);
continue;
}
} else {
console.error(`Server ${server} returned status ${response.status}`);
continue;
}
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
console.log(`Data not found on server ${server} for key ${key}`);
continue; // Try next server
} else if (error.response?.status) {
console.error(`Server ${server} error ${error.response.status}:`, error.response.statusText);
continue;
} else {
console.error(`Network error connecting to ${server}:`, error.message);
continue;
}
} else {
console.error(`Unexpected error retrieving data from ${server}:`, error);
continue;
}
}
}
return null;
}
interface TestResponse {
key: string;
value: boolean;
}
export async function testData(servers: string[], key: string): Promise<Record<string, boolean | null> | null> {
const res: Record<string, boolean | null> = {};
for (const server of servers) {
res[server] = null;
try {
const response = await axios.get(`${server}/test/${key}`);
if (response.status !== 200) {
console.error(`${server}: Test response status: ${response.status}`);
continue;
}
const data: TestResponse = response.data;
res[server] = data.value;
} catch (error) {
console.error('Error retrieving data:', error);
return null;
}
}
return res;
}

View File

@ -1,21 +1,6 @@
// Server-specific utility functions
export const EMPTY32BYTES = String('').padStart(64, '0');
export function splitPrivateData(data: Record<string, any>, privateFields: string[]): { privateData: Record<string, any>, publicData: Record<string, any> } {
const privateData: Record<string, any> = {};
const publicData: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
if (privateFields.includes(key)) {
privateData[key] = value;
} else {
publicData[key] = value;
}
}
return { privateData, publicData };
}
export function isValid32ByteHex(value: string): boolean {
// Check if the value is a valid 32-byte hex string (64 characters)
const hexRegex = /^[0-9a-fA-F]{64}$/;