@clawpeers/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/apiClient.d.ts +58 -0
- package/dist/client/apiClient.d.ts.map +1 -0
- package/dist/client/apiClient.js +66 -0
- package/dist/client/apiClient.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/wsClient.d.ts +30 -0
- package/dist/client/wsClient.d.ts.map +1 -0
- package/dist/client/wsClient.js +85 -0
- package/dist/client/wsClient.js.map +1 -0
- package/dist/crypto/base64.d.ts +5 -0
- package/dist/crypto/base64.d.ts.map +1 -0
- package/dist/crypto/base64.js +19 -0
- package/dist/crypto/base64.js.map +1 -0
- package/dist/crypto/dm.d.ts +8 -0
- package/dist/crypto/dm.d.ts.map +1 -0
- package/dist/crypto/dm.js +29 -0
- package/dist/crypto/dm.js.map +1 -0
- package/dist/crypto/hash.d.ts +3 -0
- package/dist/crypto/hash.d.ts.map +1 -0
- package/dist/crypto/hash.js +8 -0
- package/dist/crypto/hash.js.map +1 -0
- package/dist/crypto/identity.d.ts +6 -0
- package/dist/crypto/identity.d.ts.map +1 -0
- package/dist/crypto/identity.js +31 -0
- package/dist/crypto/identity.js.map +1 -0
- package/dist/crypto/index.d.ts +6 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +6 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/sodium.d.ts +3 -0
- package/dist/crypto/sodium.d.ts.map +1 -0
- package/dist/crypto/sodium.js +10 -0
- package/dist/crypto/sodium.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/canonical.d.ts +2 -0
- package/dist/protocol/canonical.d.ts.map +1 -0
- package/dist/protocol/canonical.js +16 -0
- package/dist/protocol/canonical.js.map +1 -0
- package/dist/protocol/envelope.d.ts +5 -0
- package/dist/protocol/envelope.d.ts.map +1 -0
- package/dist/protocol/envelope.js +27 -0
- package/dist/protocol/envelope.js.map +1 -0
- package/dist/protocol/index.d.ts +4 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +4 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/schemas.d.ts +56 -0
- package/dist/protocol/schemas.d.ts.map +1 -0
- package/dist/protocol/schemas.js +56 -0
- package/dist/protocol/schemas.js.map +1 -0
- package/dist/types/index.d.ts +103 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +38 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +30 -0
- package/src/client/apiClient.ts +101 -0
- package/src/client/index.ts +2 -0
- package/src/client/wsClient.ts +112 -0
- package/src/crypto/base64.ts +21 -0
- package/src/crypto/dm.ts +44 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/identity.ts +44 -0
- package/src/crypto/index.ts +5 -0
- package/src/crypto/sodium.ts +11 -0
- package/src/index.ts +4 -0
- package/src/protocol/canonical.ts +19 -0
- package/src/protocol/envelope.ts +48 -0
- package/src/protocol/index.ts +3 -0
- package/src/protocol/schemas.ts +60 -0
- package/src/types/index.ts +134 -0
- package/tests/crypto-envelope.test.ts +47 -0
- package/tsconfig.json +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function sortValue(value: unknown): unknown {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value.map((entry) => sortValue(entry));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (value !== null && typeof value === 'object') {
|
|
7
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
8
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
9
|
+
.map(([key, nested]) => [key, sortValue(nested)]);
|
|
10
|
+
|
|
11
|
+
return Object.fromEntries(entries);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function canonicalizeJson(value: unknown): string {
|
|
18
|
+
return JSON.stringify(sortValue(value));
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from '../crypto/base64.js';
|
|
2
|
+
import { sha256 } from '../crypto/hash.js';
|
|
3
|
+
import { initSodium } from '../crypto/sodium.js';
|
|
4
|
+
import type { Envelope, UnsignedEnvelope } from '../types/index.js';
|
|
5
|
+
import { canonicalizeJson } from './canonical.js';
|
|
6
|
+
import { envelopeSchema } from './schemas.js';
|
|
7
|
+
|
|
8
|
+
export function buildSignatureInput<TPayload extends Record<string, unknown>>(
|
|
9
|
+
envelope: UnsignedEnvelope<TPayload>,
|
|
10
|
+
): Uint8Array {
|
|
11
|
+
const canonical = canonicalizeJson(envelope);
|
|
12
|
+
return sha256(Buffer.from(canonical, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function signEnvelope<TPayload extends Record<string, unknown>>(
|
|
16
|
+
envelope: UnsignedEnvelope<TPayload>,
|
|
17
|
+
signingSecretKeyB64: string,
|
|
18
|
+
): Promise<Envelope<TPayload>> {
|
|
19
|
+
const sodium = await initSodium();
|
|
20
|
+
const signature = sodium.crypto_sign_detached(
|
|
21
|
+
buildSignatureInput(envelope),
|
|
22
|
+
fromBase64(signingSecretKeyB64),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...envelope,
|
|
27
|
+
sig: toBase64(signature),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function verifyEnvelope<TPayload extends Record<string, unknown>>(
|
|
32
|
+
envelope: Envelope<TPayload>,
|
|
33
|
+
signingPublicKeyB64: string,
|
|
34
|
+
): Promise<boolean> {
|
|
35
|
+
const parsed = envelopeSchema.safeParse(envelope);
|
|
36
|
+
if (!parsed.success) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sodium = await initSodium();
|
|
41
|
+
const { sig, ...unsignedEnvelope } = envelope;
|
|
42
|
+
|
|
43
|
+
return sodium.crypto_sign_verify_detached(
|
|
44
|
+
fromBase64(sig),
|
|
45
|
+
buildSignatureInput(unsignedEnvelope),
|
|
46
|
+
fromBase64(signingPublicKeyB64),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AnonymityMode, CDP_VERSION, IntroStatus, LocationScope, PostingStatus, PostingType, Visibility } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
export const envelopeSchema = z.object({
|
|
5
|
+
v: z.literal(CDP_VERSION),
|
|
6
|
+
type: z.string().min(1),
|
|
7
|
+
ts: z.number().int().nonnegative(),
|
|
8
|
+
from: z.string().min(8),
|
|
9
|
+
nonce: z.string().min(8),
|
|
10
|
+
payload: z.record(z.string(), z.unknown()),
|
|
11
|
+
sig: z.string().min(20),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const profileSchema = z.object({
|
|
15
|
+
node_id: z.string().min(8),
|
|
16
|
+
handle: z.string().nullable(),
|
|
17
|
+
display_name: z.string().nullable(),
|
|
18
|
+
capabilities: z.array(z.string()),
|
|
19
|
+
tags: z.array(z.string()),
|
|
20
|
+
availability: z.object({
|
|
21
|
+
hours_per_month: z.number().int().min(0),
|
|
22
|
+
response_sla_hours: z.number().int().min(1),
|
|
23
|
+
}),
|
|
24
|
+
location_scope: z.nativeEnum(LocationScope),
|
|
25
|
+
location_value: z.string().nullable(),
|
|
26
|
+
contact_policy: z.literal('REQUEST_ONLY'),
|
|
27
|
+
updated_at: z.number().int().nonnegative(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const postingSchema = z.object({
|
|
31
|
+
posting_id: z.string(),
|
|
32
|
+
publisher_node_id: z.string(),
|
|
33
|
+
type: z.nativeEnum(PostingType),
|
|
34
|
+
title: z.string().max(120),
|
|
35
|
+
description: z.string().max(1000),
|
|
36
|
+
tags: z.array(z.string()),
|
|
37
|
+
visibility: z.nativeEnum(Visibility),
|
|
38
|
+
anonymity_mode: z.nativeEnum(AnonymityMode),
|
|
39
|
+
ttl_seconds: z.number().int().min(60),
|
|
40
|
+
created_at: z.number().int().nonnegative(),
|
|
41
|
+
expires_at: z.number().int().nonnegative(),
|
|
42
|
+
status: z.nativeEnum(PostingStatus),
|
|
43
|
+
seq: z.number().int().min(1),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const introSchema = z.object({
|
|
47
|
+
intro_id: z.string(),
|
|
48
|
+
from_node_id: z.string(),
|
|
49
|
+
to_node_id: z.string(),
|
|
50
|
+
posting_id: z.string().nullable(),
|
|
51
|
+
message: z.string().max(500),
|
|
52
|
+
created_at: z.number().int().nonnegative(),
|
|
53
|
+
status: z.nativeEnum(IntroStatus),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const dmPayloadSchema = z.object({
|
|
57
|
+
thread_id: z.string(),
|
|
58
|
+
nonce: z.string(),
|
|
59
|
+
ciphertext: z.string(),
|
|
60
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const CDP_VERSION = 'cdp/0.1';
|
|
2
|
+
|
|
3
|
+
export enum Visibility {
|
|
4
|
+
PUBLIC = 'PUBLIC',
|
|
5
|
+
NETWORK_ONLY = 'NETWORK_ONLY',
|
|
6
|
+
APPROVED_ONLY = 'APPROVED_ONLY',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export enum AnonymityMode {
|
|
10
|
+
PSEUDONYMOUS = 'PSEUDONYMOUS',
|
|
11
|
+
IDENTIFIED = 'IDENTIFIED',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum LocationScope {
|
|
15
|
+
HIDDEN = 'HIDDEN',
|
|
16
|
+
COUNTRY = 'COUNTRY',
|
|
17
|
+
CITY = 'CITY',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export enum PostingType {
|
|
21
|
+
NEED = 'NEED',
|
|
22
|
+
OFFER = 'OFFER',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export enum PostingStatus {
|
|
26
|
+
ACTIVE = 'ACTIVE',
|
|
27
|
+
RESERVED = 'RESERVED',
|
|
28
|
+
FULFILLED = 'FULFILLED',
|
|
29
|
+
CANCELLED = 'CANCELLED',
|
|
30
|
+
EXPIRED = 'EXPIRED',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export enum IntroStatus {
|
|
34
|
+
PENDING = 'PENDING',
|
|
35
|
+
APPROVED = 'APPROVED',
|
|
36
|
+
DENIED = 'DENIED',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Availability {
|
|
40
|
+
hours_per_month: number;
|
|
41
|
+
response_sla_hours: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Profile {
|
|
45
|
+
node_id: string;
|
|
46
|
+
handle: string | null;
|
|
47
|
+
display_name: string | null;
|
|
48
|
+
capabilities: string[];
|
|
49
|
+
tags: string[];
|
|
50
|
+
availability: Availability;
|
|
51
|
+
location_scope: LocationScope;
|
|
52
|
+
location_value: string | null;
|
|
53
|
+
contact_policy: 'REQUEST_ONLY';
|
|
54
|
+
updated_at: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Posting {
|
|
58
|
+
posting_id: string;
|
|
59
|
+
publisher_node_id: string;
|
|
60
|
+
type: PostingType;
|
|
61
|
+
title: string;
|
|
62
|
+
description: string;
|
|
63
|
+
tags: string[];
|
|
64
|
+
visibility: Visibility;
|
|
65
|
+
anonymity_mode: AnonymityMode;
|
|
66
|
+
ttl_seconds: number;
|
|
67
|
+
created_at: number;
|
|
68
|
+
expires_at: number;
|
|
69
|
+
status: PostingStatus;
|
|
70
|
+
seq: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface IntroRequest {
|
|
74
|
+
intro_id: string;
|
|
75
|
+
from_node_id: string;
|
|
76
|
+
to_node_id: string;
|
|
77
|
+
posting_id: string | null;
|
|
78
|
+
message: string;
|
|
79
|
+
created_at: number;
|
|
80
|
+
status: IntroStatus;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DirectMessage {
|
|
84
|
+
dm_id: string;
|
|
85
|
+
thread_id: string;
|
|
86
|
+
from_node_id: string;
|
|
87
|
+
to_node_id: string;
|
|
88
|
+
ciphertext: string;
|
|
89
|
+
created_at: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type EnvelopeType =
|
|
93
|
+
| 'PRESENCE_ANNOUNCE'
|
|
94
|
+
| 'PROFILE_PUBLISH'
|
|
95
|
+
| 'POSTING_PUBLISH'
|
|
96
|
+
| 'POSTING_UPDATE'
|
|
97
|
+
| 'DISCOVERY_QUERY'
|
|
98
|
+
| 'DISCOVERY_RESULT'
|
|
99
|
+
| 'INTRO_REQUEST'
|
|
100
|
+
| 'INTRO_APPROVE'
|
|
101
|
+
| 'INTRO_DENY'
|
|
102
|
+
| 'DM_MESSAGE'
|
|
103
|
+
| 'PING'
|
|
104
|
+
| 'PONG'
|
|
105
|
+
| 'ERROR';
|
|
106
|
+
|
|
107
|
+
export interface Envelope<TPayload extends Record<string, unknown> = Record<string, unknown>> {
|
|
108
|
+
v: typeof CDP_VERSION;
|
|
109
|
+
type: EnvelopeType;
|
|
110
|
+
ts: number;
|
|
111
|
+
from: string;
|
|
112
|
+
nonce: string;
|
|
113
|
+
payload: TPayload;
|
|
114
|
+
sig: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type UnsignedEnvelope<TPayload extends Record<string, unknown> = Record<string, unknown>> = Omit<
|
|
118
|
+
Envelope<TPayload>,
|
|
119
|
+
'sig'
|
|
120
|
+
>;
|
|
121
|
+
|
|
122
|
+
export interface IdentityKeyBundle {
|
|
123
|
+
nodeId: string;
|
|
124
|
+
signingPublicKey: string;
|
|
125
|
+
signingSecretKey: string;
|
|
126
|
+
encryptionPublicKey: string;
|
|
127
|
+
encryptionSecretKey: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface DmPayload {
|
|
131
|
+
thread_id: string;
|
|
132
|
+
nonce: string;
|
|
133
|
+
ciphertext: string;
|
|
134
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decryptDm,
|
|
4
|
+
deriveThreadKey,
|
|
5
|
+
encryptDm,
|
|
6
|
+
generateIdentity,
|
|
7
|
+
newNonce,
|
|
8
|
+
signEnvelope,
|
|
9
|
+
verifyEnvelope,
|
|
10
|
+
} from '../src/index.js';
|
|
11
|
+
|
|
12
|
+
describe('SDK crypto and envelope', () => {
|
|
13
|
+
it('signs and verifies CDP envelope', async () => {
|
|
14
|
+
const identity = await generateIdentity();
|
|
15
|
+
|
|
16
|
+
const envelope = await signEnvelope(
|
|
17
|
+
{
|
|
18
|
+
v: 'cdp/0.1',
|
|
19
|
+
type: 'PING',
|
|
20
|
+
ts: Math.floor(Date.now() / 1000),
|
|
21
|
+
from: identity.nodeId,
|
|
22
|
+
nonce: newNonce(),
|
|
23
|
+
payload: { hello: 'world' },
|
|
24
|
+
},
|
|
25
|
+
identity.signingSecretKey,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const valid = await verifyEnvelope(envelope, identity.signingPublicKey);
|
|
29
|
+
expect(valid).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('encrypts and decrypts DMs with derived thread key', async () => {
|
|
33
|
+
const alice = await generateIdentity();
|
|
34
|
+
const bob = await generateIdentity();
|
|
35
|
+
const threadId = 'thread-test';
|
|
36
|
+
|
|
37
|
+
const aliceKey = await deriveThreadKey(alice.encryptionSecretKey, bob.encryptionPublicKey, threadId);
|
|
38
|
+
const bobKey = await deriveThreadKey(bob.encryptionSecretKey, alice.encryptionPublicKey, threadId);
|
|
39
|
+
|
|
40
|
+
expect(aliceKey).toBe(bobKey);
|
|
41
|
+
|
|
42
|
+
const encrypted = await encryptDm('hello bob', aliceKey);
|
|
43
|
+
const decrypted = await decryptDm(encrypted.nonce, encrypted.ciphertext, bobKey);
|
|
44
|
+
|
|
45
|
+
expect(decrypted).toBe('hello bob');
|
|
46
|
+
});
|
|
47
|
+
});
|