trustplane-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/README.md +57 -0
- package/index.d.ts +44 -0
- package/index.js +209 -0
- package/package.json +23 -0
- package/test_vector.js +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Trustplane JS SDK (v0.1)
|
|
2
|
+
|
|
3
|
+
Minimal SDK to generate Trustplane proof headers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install trustplane-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { sign } = require('trustplane-sdk');
|
|
15
|
+
|
|
16
|
+
const out = sign({
|
|
17
|
+
tenantId: 'mergematter.io',
|
|
18
|
+
apiId: 'api_demo',
|
|
19
|
+
clientId: 'client_demo',
|
|
20
|
+
privateKey: '<private_key_b64url>',
|
|
21
|
+
method: 'GET',
|
|
22
|
+
path: '/orders',
|
|
23
|
+
body: ''
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log(out.headers);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Browser (async)
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { signAsync } from "trustplane-sdk";
|
|
33
|
+
|
|
34
|
+
const out = await signAsync({
|
|
35
|
+
tenantId: "mergematter.io",
|
|
36
|
+
apiId: "api_demo",
|
|
37
|
+
clientId: "client_demo",
|
|
38
|
+
privateKey: "<private_key_b64url>",
|
|
39
|
+
method: "GET",
|
|
40
|
+
path: "/orders",
|
|
41
|
+
body: ""
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Config file
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const { fromFile } = require('trustplane-sdk');
|
|
49
|
+
|
|
50
|
+
const client = fromFile('./trustplane.json');
|
|
51
|
+
const out = client.sign({
|
|
52
|
+
method: 'GET',
|
|
53
|
+
path: '/orders',
|
|
54
|
+
body: '',
|
|
55
|
+
privateKey: '<private_key_b64url>'
|
|
56
|
+
});
|
|
57
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type SignInput = {
|
|
2
|
+
tenantId: string;
|
|
3
|
+
apiId: string;
|
|
4
|
+
clientId: string;
|
|
5
|
+
privateKey: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
path: string;
|
|
8
|
+
body?: string;
|
|
9
|
+
bucketSeconds?: number;
|
|
10
|
+
timeBucketOverride?: number;
|
|
11
|
+
nonceOverride?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type SignOutput = {
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
verifyPayload: {
|
|
17
|
+
tenant_id: string;
|
|
18
|
+
api_id: string;
|
|
19
|
+
client_id: string;
|
|
20
|
+
method: string;
|
|
21
|
+
path: string;
|
|
22
|
+
body_hash: string;
|
|
23
|
+
time_bucket: number;
|
|
24
|
+
nonce: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
};
|
|
27
|
+
transcript: string;
|
|
28
|
+
digest: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function sign(input: SignInput): SignOutput;
|
|
32
|
+
export function signAsync(input: SignInput): Promise<SignOutput>;
|
|
33
|
+
|
|
34
|
+
export function createClient(input: {
|
|
35
|
+
tenantId: string;
|
|
36
|
+
apiId: string;
|
|
37
|
+
clientId: string;
|
|
38
|
+
bucketSeconds?: number;
|
|
39
|
+
}): {
|
|
40
|
+
sign(input: Omit<SignInput, "tenantId" | "apiId" | "clientId" | "bucketSeconds">): SignOutput;
|
|
41
|
+
signAsync(input: Omit<SignInput, "tenantId" | "apiId" | "clientId" | "bucketSeconds">): Promise<SignOutput>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function fromFile(path: string): ReturnType<typeof createClient>;
|
package/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const nacl = require('tweetnacl');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function base64urlEncode(buf) {
|
|
6
|
+
return Buffer.from(buf)
|
|
7
|
+
.toString('base64')
|
|
8
|
+
.replace(/\+/g, '-')
|
|
9
|
+
.replace(/\//g, '_')
|
|
10
|
+
.replace(/=+$/g, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function base64urlDecode(str) {
|
|
14
|
+
const pad = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4));
|
|
15
|
+
const b64 = (str + pad).replace(/-/g, '+').replace(/_/g, '/');
|
|
16
|
+
return Buffer.from(b64, 'base64');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sha256Base64url(data) {
|
|
20
|
+
const digest = crypto.createHash('sha256').update(data).digest();
|
|
21
|
+
return base64urlEncode(digest);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildTranscript(input) {
|
|
25
|
+
return [
|
|
26
|
+
'TP1',
|
|
27
|
+
`tenant_id=${input.tenantId}`,
|
|
28
|
+
`api_id=${input.apiId}`,
|
|
29
|
+
`client_id=${input.clientId}`,
|
|
30
|
+
`method=${input.method}`,
|
|
31
|
+
`path=${input.path}`,
|
|
32
|
+
`body_hash=${input.bodyHash}`,
|
|
33
|
+
`time_bucket=${input.timeBucket}`,
|
|
34
|
+
`nonce=${input.nonce}`,
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function randomNonce() {
|
|
39
|
+
return base64urlEncode(crypto.randomBytes(16));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function signProof({ tenantId, apiId, clientId, privateKey, method, path, body, bucketSeconds, timeBucketOverride, nonceOverride }) {
|
|
43
|
+
if (!tenantId || !apiId || !clientId) {
|
|
44
|
+
throw new Error('tenantId, apiId, and clientId are required');
|
|
45
|
+
}
|
|
46
|
+
if (!privateKey) {
|
|
47
|
+
throw new Error('privateKey is required');
|
|
48
|
+
}
|
|
49
|
+
if (!path) {
|
|
50
|
+
throw new Error('path is required');
|
|
51
|
+
}
|
|
52
|
+
const methodUpper = (method || 'GET').toUpperCase();
|
|
53
|
+
const bodyStr = body || '';
|
|
54
|
+
const bodyHash = `sha256:${sha256Base64url(Buffer.from(bodyStr))}`;
|
|
55
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
56
|
+
const bucket = bucketSeconds > 0 ? bucketSeconds : 20;
|
|
57
|
+
const timeBucket = Number.isFinite(timeBucketOverride) ? timeBucketOverride : Math.floor(nowSeconds / bucket);
|
|
58
|
+
const nonce = nonceOverride || randomNonce();
|
|
59
|
+
|
|
60
|
+
const transcript = buildTranscript({
|
|
61
|
+
tenantId,
|
|
62
|
+
apiId,
|
|
63
|
+
clientId,
|
|
64
|
+
method: methodUpper,
|
|
65
|
+
path,
|
|
66
|
+
bodyHash,
|
|
67
|
+
timeBucket,
|
|
68
|
+
nonce,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const digest = crypto.createHash('sha256').update(transcript).digest();
|
|
72
|
+
const priv = base64urlDecode(privateKey);
|
|
73
|
+
if (priv.length !== 64) {
|
|
74
|
+
throw new Error('privateKey must be ed25519 private key (64 bytes, base64url)');
|
|
75
|
+
}
|
|
76
|
+
const sig = nacl.sign.detached(digest, new Uint8Array(priv));
|
|
77
|
+
const signature = base64urlEncode(Buffer.from(sig));
|
|
78
|
+
|
|
79
|
+
const headers = {
|
|
80
|
+
'x-tp-tenant-id': tenantId,
|
|
81
|
+
'x-tp-api-id': apiId,
|
|
82
|
+
'x-tp-client-id': clientId,
|
|
83
|
+
'x-tp-time-bucket': String(timeBucket),
|
|
84
|
+
'x-tp-nonce': nonce,
|
|
85
|
+
'x-tp-signature': signature,
|
|
86
|
+
'x-tp-body-hash': bodyHash,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const verifyPayload = {
|
|
90
|
+
tenant_id: tenantId,
|
|
91
|
+
api_id: apiId,
|
|
92
|
+
client_id: clientId,
|
|
93
|
+
method: methodUpper,
|
|
94
|
+
path,
|
|
95
|
+
body_hash: bodyHash,
|
|
96
|
+
time_bucket: timeBucket,
|
|
97
|
+
nonce,
|
|
98
|
+
signature,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
headers,
|
|
103
|
+
verifyPayload,
|
|
104
|
+
transcript,
|
|
105
|
+
digest: base64urlEncode(digest),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function sha256Base64urlAsync(data) {
|
|
110
|
+
if (!globalThis.crypto || !globalThis.crypto.subtle) {
|
|
111
|
+
throw new Error('WebCrypto is required for async hashing in browsers');
|
|
112
|
+
}
|
|
113
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', data);
|
|
114
|
+
return base64urlEncode(Buffer.from(digest));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function signProofAsync({ tenantId, apiId, clientId, privateKey, method, path, body, bucketSeconds, timeBucketOverride, nonceOverride }) {
|
|
118
|
+
if (!tenantId || !apiId || !clientId) {
|
|
119
|
+
throw new Error('tenantId, apiId, and clientId are required');
|
|
120
|
+
}
|
|
121
|
+
if (!privateKey) {
|
|
122
|
+
throw new Error('privateKey is required');
|
|
123
|
+
}
|
|
124
|
+
if (!path) {
|
|
125
|
+
throw new Error('path is required');
|
|
126
|
+
}
|
|
127
|
+
const methodUpper = (method || 'GET').toUpperCase();
|
|
128
|
+
const bodyStr = body || '';
|
|
129
|
+
const bodyHash = `sha256:${await sha256Base64urlAsync(Buffer.from(bodyStr))}`;
|
|
130
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
131
|
+
const bucket = bucketSeconds > 0 ? bucketSeconds : 20;
|
|
132
|
+
const timeBucket = Number.isFinite(timeBucketOverride) ? timeBucketOverride : Math.floor(nowSeconds / bucket);
|
|
133
|
+
const nonce = nonceOverride || randomNonce();
|
|
134
|
+
|
|
135
|
+
const transcript = buildTranscript({
|
|
136
|
+
tenantId,
|
|
137
|
+
apiId,
|
|
138
|
+
clientId,
|
|
139
|
+
method: methodUpper,
|
|
140
|
+
path,
|
|
141
|
+
bodyHash,
|
|
142
|
+
timeBucket,
|
|
143
|
+
nonce,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const digest = crypto.createHash('sha256').update(transcript).digest();
|
|
147
|
+
const priv = base64urlDecode(privateKey);
|
|
148
|
+
if (priv.length !== 64) {
|
|
149
|
+
throw new Error('privateKey must be ed25519 private key (64 bytes, base64url)');
|
|
150
|
+
}
|
|
151
|
+
const sig = nacl.sign.detached(digest, new Uint8Array(priv));
|
|
152
|
+
const signature = base64urlEncode(Buffer.from(sig));
|
|
153
|
+
|
|
154
|
+
const headers = {
|
|
155
|
+
'x-tp-tenant-id': tenantId,
|
|
156
|
+
'x-tp-api-id': apiId,
|
|
157
|
+
'x-tp-client-id': clientId,
|
|
158
|
+
'x-tp-time-bucket': String(timeBucket),
|
|
159
|
+
'x-tp-nonce': nonce,
|
|
160
|
+
'x-tp-signature': signature,
|
|
161
|
+
'x-tp-body-hash': bodyHash,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const verifyPayload = {
|
|
165
|
+
tenant_id: tenantId,
|
|
166
|
+
api_id: apiId,
|
|
167
|
+
client_id: clientId,
|
|
168
|
+
method: methodUpper,
|
|
169
|
+
path,
|
|
170
|
+
body_hash: bodyHash,
|
|
171
|
+
time_bucket: timeBucket,
|
|
172
|
+
nonce,
|
|
173
|
+
signature,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
headers,
|
|
178
|
+
verifyPayload,
|
|
179
|
+
transcript,
|
|
180
|
+
digest: base64urlEncode(digest),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function fromFile(path) {
|
|
185
|
+
const raw = fs.readFileSync(path, 'utf8');
|
|
186
|
+
const cfg = JSON.parse(raw);
|
|
187
|
+
return createClient({
|
|
188
|
+
tenantId: cfg.tenant_id,
|
|
189
|
+
apiId: cfg.api_id,
|
|
190
|
+
clientId: cfg.client_id,
|
|
191
|
+
bucketSeconds: cfg.bucket_seconds,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createClient({ tenantId, apiId, clientId, bucketSeconds }) {
|
|
196
|
+
return {
|
|
197
|
+
sign: ({ method, path, body, privateKey }) =>
|
|
198
|
+
signProof({ tenantId, apiId, clientId, privateKey, method, path, body, bucketSeconds }),
|
|
199
|
+
signAsync: ({ method, path, body, privateKey }) =>
|
|
200
|
+
signProofAsync({ tenantId, apiId, clientId, privateKey, method, path, body, bucketSeconds }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
sign: signProof,
|
|
206
|
+
signAsync: signProofAsync,
|
|
207
|
+
createClient,
|
|
208
|
+
fromFile,
|
|
209
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trustplane-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Trustplane SDK (JS) for generating request proof headers",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"index.d.ts",
|
|
10
|
+
"README.md",
|
|
11
|
+
"test_vector.js"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node test_vector.js"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"tweetnacl": "^1.0.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/test_vector.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { sign } = require('./index');
|
|
2
|
+
|
|
3
|
+
const out = sign({
|
|
4
|
+
tenantId: 'mergematter.io',
|
|
5
|
+
apiId: 'api_demo',
|
|
6
|
+
clientId: 'client_demo',
|
|
7
|
+
privateKey: 'dyIi-Xwr2tl6NdzkT0CXWUZkb9cPRXTSbDQgz-SCcORqDZPYyFAEXEGn6Eg6hlokBxvS8avUjpmk4VwaXmg7zQ',
|
|
8
|
+
method: 'GET',
|
|
9
|
+
path: '/orders',
|
|
10
|
+
body: '',
|
|
11
|
+
bucketSeconds: 20,
|
|
12
|
+
timeBucketOverride: 88498363,
|
|
13
|
+
nonceOverride: '9biCQnN2URhTCqhn_-orBQ'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
console.log(out.headers);
|
|
17
|
+
console.log(out.verifyPayload);
|
|
18
|
+
console.log('transcript:', out.transcript);
|
|
19
|
+
console.log('digest_b64url:', out.digest);
|