Skip to content

Commit e54f04f

Browse files
committed
feat: implement @nse-dev/core types + @nse-dev/server with full test suite
Core (9 tests): NSEProvider interface, NSEEvent/NSESignedEvent/NSEKeyInfo types aligned with nostr-crypto-utils, NSEEncryptedBlob format, NSEError with error codes, NSEStorage interface. Server (23 tests): NSEServer using nostr-crypto-utils for Schnorr signing, AES-256-GCM key wrapping via @noble/hashes, generate/sign/verify round-trip, memory zeroing, wrong-key rejection, NSEMemoryStorage for testing. Note: uses signEvent instead of finalizeEvent to avoid kind 0 falsy bug in nostr-crypto-utils (event.kind || 1 treats 0 as falsy).
1 parent e10b48e commit e54f04f

13 files changed

Lines changed: 4430 additions & 95 deletions

platforms/core/package-lock.json

Lines changed: 1284 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

platforms/core/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@nse-dev/core",
3+
"version": "0.1.0",
4+
"description": "Shared types for Nostr Secure Enclave — hardware-backed key management for Nostr",
5+
"type": "module",
6+
"main": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"import": "./dist/index.js",
11+
"types": "./dist/index.d.ts"
12+
}
13+
},
14+
"files": ["dist"],
15+
"scripts": {
16+
"build": "tsc",
17+
"test": "vitest run",
18+
"test:watch": "vitest"
19+
},
20+
"keywords": ["nostr", "secure-enclave", "key-management", "hardware-security"],
21+
"license": "MIT",
22+
"repository": {
23+
"type": "git",
24+
"url": "https://github.com/HumanjavaEnterprises/nse-dev.web.landingpage.src",
25+
"directory": "platforms/core"
26+
},
27+
"devDependencies": {
28+
"typescript": "^5.7.0",
29+
"vitest": "^4.0.0"
30+
},
31+
"peerDependencies": {
32+
"nostr-crypto-utils": "^0.7.0"
33+
},
34+
"peerDependenciesMeta": {
35+
"nostr-crypto-utils": {
36+
"optional": true
37+
}
38+
}
39+
}

platforms/core/src/index.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { NSEError, NSEErrorCode } from './index.js';
3+
import type {
4+
NSEEvent,
5+
NSESignedEvent,
6+
NSEKeyInfo,
7+
NSEProvider,
8+
NSEStorage,
9+
NSEEncryptedBlob,
10+
} from './index.js';
11+
12+
describe('@nse-dev/core types', () => {
13+
it('NSEEvent is compatible with nostr-crypto-utils BaseNostrEvent', () => {
14+
const event: NSEEvent = {
15+
kind: 1,
16+
content: 'hello nostr',
17+
tags: [['p', 'abc123']],
18+
created_at: Math.floor(Date.now() / 1000),
19+
};
20+
expect(event.kind).toBe(1);
21+
expect(event.content).toBe('hello nostr');
22+
expect(event.tags).toHaveLength(1);
23+
expect(typeof event.created_at).toBe('number');
24+
});
25+
26+
it('NSEEvent accepts optional pubkey', () => {
27+
const event: NSEEvent = {
28+
kind: 0,
29+
content: '{}',
30+
tags: [],
31+
created_at: 1710000000,
32+
pubkey: 'aabbccdd'.repeat(8),
33+
};
34+
expect(event.pubkey).toHaveLength(64);
35+
});
36+
37+
it('NSESignedEvent has all required fields', () => {
38+
const signed: NSESignedEvent = {
39+
id: 'a'.repeat(64),
40+
pubkey: 'b'.repeat(64),
41+
sig: 'c'.repeat(128),
42+
kind: 1,
43+
content: 'test',
44+
tags: [],
45+
created_at: 1710000000,
46+
};
47+
expect(signed.id).toHaveLength(64);
48+
expect(signed.pubkey).toHaveLength(64);
49+
expect(signed.sig).toHaveLength(128);
50+
});
51+
52+
it('NSEKeyInfo includes hardware_backed flag', () => {
53+
const info: NSEKeyInfo = {
54+
pubkey: 'a'.repeat(64),
55+
npub: 'npub1' + 'x'.repeat(58),
56+
created_at: 1710000000,
57+
hardware_backed: true,
58+
};
59+
expect(info.hardware_backed).toBe(true);
60+
});
61+
62+
it('NSEEncryptedBlob has version 1', () => {
63+
const blob: NSEEncryptedBlob = {
64+
version: 1,
65+
ciphertext: 'base64encoded==',
66+
iv: 'aWluaXR2ZWN0',
67+
pubkey: 'a'.repeat(64),
68+
npub: 'npub1' + 'x'.repeat(58),
69+
created_at: 1710000000,
70+
hardware_backed: true,
71+
};
72+
expect(blob.version).toBe(1);
73+
});
74+
75+
it('NSEError carries error code', () => {
76+
const err = new NSEError('No key found', NSEErrorCode.KEY_NOT_FOUND);
77+
expect(err.message).toBe('No key found');
78+
expect(err.code).toBe('KEY_NOT_FOUND');
79+
expect(err.name).toBe('NSEError');
80+
expect(err instanceof Error).toBe(true);
81+
});
82+
83+
it('NSEErrorCode has all expected codes', () => {
84+
const codes = Object.values(NSEErrorCode);
85+
expect(codes).toContain('KEY_NOT_FOUND');
86+
expect(codes).toContain('AUTH_FAILED');
87+
expect(codes).toContain('HARDWARE_UNAVAILABLE');
88+
expect(codes).toContain('KEY_EXISTS');
89+
expect(codes).toContain('DECRYPTION_FAILED');
90+
expect(codes).toContain('STORAGE_ERROR');
91+
expect(codes).toContain('SIGN_FAILED');
92+
expect(codes).toHaveLength(7);
93+
});
94+
95+
it('NSEStorage interface is structurally valid', () => {
96+
// Verify the interface works with a mock implementation
97+
const storage: NSEStorage = {
98+
get: async (key: string) => key === 'exists' ? 'value' : null,
99+
put: async (_key: string, _value: string) => {},
100+
delete: async (_key: string) => {},
101+
};
102+
expect(storage.get).toBeDefined();
103+
expect(storage.put).toBeDefined();
104+
expect(storage.delete).toBeDefined();
105+
});
106+
107+
it('NSEProvider interface accepts a mock implementation', () => {
108+
// Structural type check — a mock provider satisfies the interface
109+
const mock: NSEProvider = {
110+
generate: async () => ({
111+
pubkey: 'a'.repeat(64),
112+
npub: 'npub1test',
113+
created_at: Date.now(),
114+
hardware_backed: false,
115+
}),
116+
sign: async (event) => ({
117+
...event,
118+
id: 'a'.repeat(64),
119+
pubkey: 'b'.repeat(64),
120+
sig: 'c'.repeat(128),
121+
}),
122+
getPublicKey: async () => 'a'.repeat(64),
123+
getNpub: async () => 'npub1test',
124+
exists: async () => true,
125+
destroy: async () => {},
126+
};
127+
expect(mock.generate).toBeDefined();
128+
expect(mock.sign).toBeDefined();
129+
});
130+
});

platforms/core/src/index.ts

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,77 @@
11
/**
22
* @nse-dev/core — Shared types for Nostr Secure Enclave
3+
*
4+
* These types align with nostr-crypto-utils where possible.
5+
* NSE adds hardware-backing metadata on top of standard Nostr types.
36
*/
47

5-
/** Minimal Nostr event for signing */
8+
// ---------------------------------------------------------------------------
9+
// Event types — compatible with nostr-crypto-utils BaseNostrEvent / SignedNostrEvent
10+
// ---------------------------------------------------------------------------
11+
12+
/** Unsigned Nostr event for signing (matches nostr-crypto-utils BaseNostrEvent) */
613
export interface NSEEvent {
714
kind: number;
815
content: string;
916
tags: string[][];
1017
created_at: number;
18+
pubkey?: string;
1119
}
1220

13-
/** Signed Nostr event (id + sig populated) */
14-
export interface NSESignedEvent extends NSEEvent {
21+
/** Signed Nostr event (matches nostr-crypto-utils SignedNostrEvent) */
22+
export interface NSESignedEvent {
1523
id: string;
1624
pubkey: string;
1725
sig: string;
26+
kind: number;
27+
content: string;
28+
tags: string[][];
29+
created_at: number;
1830
}
1931

32+
// ---------------------------------------------------------------------------
33+
// Key types
34+
// ---------------------------------------------------------------------------
35+
2036
/** Key info returned by generate() */
2137
export interface NSEKeyInfo {
38+
/** Hex pubkey (64 chars) */
2239
pubkey: string;
40+
/** Bech32 npub */
2341
npub: string;
42+
/** Unix timestamp of key creation */
2443
created_at: number;
44+
/** True if key is protected by hardware (Secure Enclave, StrongBox, TPM) */
2545
hardware_backed: boolean;
2646
}
2747

28-
/** Platform-agnostic NSE contract — every implementation conforms to this */
48+
// ---------------------------------------------------------------------------
49+
// Storage interface — each platform provides its own
50+
// ---------------------------------------------------------------------------
51+
52+
/** Platform-specific encrypted blob storage */
53+
export interface NSEStorage {
54+
get(key: string): Promise<string | null>;
55+
put(key: string, value: string): Promise<void>;
56+
delete(key: string): Promise<void>;
57+
}
58+
59+
// ---------------------------------------------------------------------------
60+
// Provider interface — the contract every platform implementation conforms to
61+
// ---------------------------------------------------------------------------
62+
63+
/** Platform-agnostic NSE contract */
2964
export interface NSEProvider {
3065
/** Generate a new secp256k1 keypair, protected by hardware */
3166
generate(): Promise<NSEKeyInfo>;
3267

33-
/** Sign a Nostr event (biometric unlock → decrypt → sign → zero) */
68+
/** Sign a Nostr event (unlock → decrypt → sign → zero) */
3469
sign(event: NSEEvent): Promise<NSESignedEvent>;
3570

36-
/** Get the hex pubkey */
71+
/** Get the hex pubkey (does not require unlock) */
3772
getPublicKey(): Promise<string>;
3873

39-
/** Get the bech32 npub */
74+
/** Get the bech32 npub (does not require unlock) */
4075
getNpub(): Promise<string>;
4176

4277
/** Check if a key exists in storage */
@@ -45,3 +80,56 @@ export interface NSEProvider {
4580
/** Wipe all key material */
4681
destroy(): Promise<void>;
4782
}
83+
84+
// ---------------------------------------------------------------------------
85+
// Encrypted blob format — what gets stored at rest
86+
// ---------------------------------------------------------------------------
87+
88+
/** The encrypted key blob stored by every platform */
89+
export interface NSEEncryptedBlob {
90+
/** Version of the blob format (for future migration) */
91+
version: 1;
92+
/** AES-GCM encrypted secp256k1 private key (base64) */
93+
ciphertext: string;
94+
/** AES-GCM initialization vector (base64, 12 bytes) */
95+
iv: string;
96+
/** Hex pubkey (stored alongside for lookup without decryption) */
97+
pubkey: string;
98+
/** Bech32 npub */
99+
npub: string;
100+
/** Unix timestamp of key creation */
101+
created_at: number;
102+
/** Whether the wrapping key is hardware-backed */
103+
hardware_backed: boolean;
104+
}
105+
106+
// ---------------------------------------------------------------------------
107+
// Error types
108+
// ---------------------------------------------------------------------------
109+
110+
export class NSEError extends Error {
111+
constructor(
112+
message: string,
113+
public readonly code: NSEErrorCode,
114+
) {
115+
super(message);
116+
this.name = 'NSEError';
117+
}
118+
}
119+
120+
export enum NSEErrorCode {
121+
/** No key exists — call generate() first */
122+
KEY_NOT_FOUND = 'KEY_NOT_FOUND',
123+
/** Biometric / unlock was denied or failed */
124+
AUTH_FAILED = 'AUTH_FAILED',
125+
/** Hardware enclave not available on this device */
126+
HARDWARE_UNAVAILABLE = 'HARDWARE_UNAVAILABLE',
127+
/** Key already exists — call destroy() first if you want to regenerate */
128+
KEY_EXISTS = 'KEY_EXISTS',
129+
/** AES-GCM decryption failed (corrupted blob or wrong wrapping key) */
130+
DECRYPTION_FAILED = 'DECRYPTION_FAILED',
131+
/** Storage read/write failed */
132+
STORAGE_ERROR = 'STORAGE_ERROR',
133+
/** Signing failed */
134+
SIGN_FAILED = 'SIGN_FAILED',
135+
}

platforms/core/tsconfig.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"moduleResolution": "bundler",
6+
"declaration": true,
7+
"declarationMap": true,
8+
"sourceMap": true,
9+
"outDir": "./dist",
10+
"rootDir": "./src",
11+
"strict": true,
12+
"esModuleInterop": true,
13+
"skipLibCheck": true,
14+
"forceConsistentCasingInFileNames": true
15+
},
16+
"include": ["src"],
17+
"exclude": ["dist", "node_modules", "**/*.test.ts"]
18+
}

0 commit comments

Comments
 (0)