@cybersidecar/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/browser/index.js +1 -0
- package/dist/core/index.js +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.js +1 -0
- package/package.json +34 -0
- package/src/sidecar_sdk_browser.js +51 -0
- package/src/sidecar_sdk_core.js +246 -0
- package/src/sidecar_sdk_node.js +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../src/sidecar_sdk_browser.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SidecarClient } from "../../src/sidecar_sdk_core.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./index.js");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../core/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cybersidecar/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/node/index.cjs",
|
|
6
|
+
"module": "./dist/node/index.js",
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/node/index.js",
|
|
11
|
+
"require": "./dist/node/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./node": {
|
|
14
|
+
"import": "./dist/node/index.js",
|
|
15
|
+
"require": "./dist/node/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./browser": {
|
|
18
|
+
"import": "./dist/browser/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"package.json"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"build:browser:single": "tsup src/browser.js --format=iife --global-name=SidecarSDK --out-dir dist/browser-single --minify"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// sidecar_sdk_browser.js (ESM)
|
|
2
|
+
import { SidecarClient } from "../src/sidecar_sdk_core.js";
|
|
3
|
+
|
|
4
|
+
const DB_NAME = "sidecar-dpop";
|
|
5
|
+
const STORE = "keys";
|
|
6
|
+
const KEY_ID = "dpop_keypair";
|
|
7
|
+
|
|
8
|
+
function openDb() {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
11
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
12
|
+
req.onsuccess = () => resolve(req.result);
|
|
13
|
+
req.onerror = () => reject(req.error);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function idbGet(db, key) {
|
|
18
|
+
return await new Promise((resolve, reject) => {
|
|
19
|
+
const tx = db.transaction(STORE, "readonly");
|
|
20
|
+
const st = tx.objectStore(STORE);
|
|
21
|
+
const req = st.get(key);
|
|
22
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
23
|
+
req.onerror = () => reject(req.error);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function idbPut(db, key, value) {
|
|
28
|
+
return await new Promise((resolve, reject) => {
|
|
29
|
+
const tx = db.transaction(STORE, "readwrite");
|
|
30
|
+
const st = tx.objectStore(STORE);
|
|
31
|
+
const req = st.put(value, key);
|
|
32
|
+
req.onsuccess = () => resolve(true);
|
|
33
|
+
req.onerror = () => reject(req.error);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const browserStorage = {
|
|
38
|
+
async get() {
|
|
39
|
+
const db = await openDb();
|
|
40
|
+
return await idbGet(db, KEY_ID);
|
|
41
|
+
},
|
|
42
|
+
async set(obj) {
|
|
43
|
+
const db = await openDb();
|
|
44
|
+
await idbPut(db, KEY_ID, obj);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { SidecarClient };
|
|
49
|
+
export function createBrowserSidecarClient(opts) {
|
|
50
|
+
return new SidecarClient({ ...opts, storage: browserStorage });
|
|
51
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// sidecar_sdk_core.js (ESM)
|
|
2
|
+
|
|
3
|
+
// ---- storage (browser/node) -------------------------------------------
|
|
4
|
+
// Core expects a store with async get(key) + set(key,value).
|
|
5
|
+
// Browser build will inject IndexedDB store.
|
|
6
|
+
// Node build must not crash if none is provided (use in-memory Map).
|
|
7
|
+
const KEYPAIR_STORAGE_KEY = "dpop:keypair";
|
|
8
|
+
const __memStore = new Map();
|
|
9
|
+
|
|
10
|
+
function normalizeStore(store) {
|
|
11
|
+
if (store && typeof store.get === "function" && typeof store.set === "function") {
|
|
12
|
+
return store;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
async get(k) {
|
|
16
|
+
return __memStore.has(k) ? __memStore.get(k) : null;
|
|
17
|
+
},
|
|
18
|
+
async set(k, v) {
|
|
19
|
+
__memStore.set(k, v);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function b64url(bytes) {
|
|
25
|
+
const bin = String.fromCharCode(...new Uint8Array(bytes));
|
|
26
|
+
const b64 = btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
27
|
+
return b64;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function sha256B64Url(str) {
|
|
31
|
+
const data = new TextEncoder().encode(str);
|
|
32
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
33
|
+
return b64url(digest);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function canonicalHtu(url) {
|
|
37
|
+
// DPoP spec: exact URL minus fragment; keep query
|
|
38
|
+
const u = new URL(url);
|
|
39
|
+
u.hash = "";
|
|
40
|
+
return u.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function exportJwk(key) {
|
|
44
|
+
return await crypto.subtle.exportKey("jwk", key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function importPrivateJwk(jwk) {
|
|
48
|
+
return await crypto.subtle.importKey(
|
|
49
|
+
"jwk",
|
|
50
|
+
jwk,
|
|
51
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
52
|
+
true,
|
|
53
|
+
["sign"]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function signEs256(privateKey, dataBytes) {
|
|
58
|
+
const sig = await crypto.subtle.sign(
|
|
59
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
60
|
+
privateKey,
|
|
61
|
+
dataBytes
|
|
62
|
+
);
|
|
63
|
+
return b64url(sig);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function getOrCreateKeypair(store) {
|
|
67
|
+
store = normalizeStore(store);
|
|
68
|
+
|
|
69
|
+
// NOTE: store.get(key) returns what you saved with store.set(key, value)
|
|
70
|
+
const saved = await store.get(KEYPAIR_STORAGE_KEY);
|
|
71
|
+
if (saved?.privateJwk && saved?.publicJwk) {
|
|
72
|
+
const privateKey = await importPrivateJwk(saved.privateJwk);
|
|
73
|
+
return { privateKey, publicJwk: saved.publicJwk };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const kp = await crypto.subtle.generateKey(
|
|
77
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
78
|
+
true,
|
|
79
|
+
["sign", "verify"]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const privateJwk = await exportJwk(kp.privateKey);
|
|
83
|
+
const publicJwk = await exportJwk(kp.publicKey);
|
|
84
|
+
|
|
85
|
+
await store.set(KEYPAIR_STORAGE_KEY, { privateJwk, publicJwk });
|
|
86
|
+
|
|
87
|
+
return { privateKey: kp.privateKey, publicJwk };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function makeDpopProof({ storage, method, url, tone, deviceId, orgId }) {
|
|
91
|
+
const store = normalizeStore(storage);
|
|
92
|
+
const { privateKey, publicJwk } = await getOrCreateKeypair(store);
|
|
93
|
+
|
|
94
|
+
const now = Math.floor(Date.now() / 1000);
|
|
95
|
+
const jti = (await sha256B64Url(`${now}:${method}:${url}:${Math.random()}`)).slice(0, 22);
|
|
96
|
+
|
|
97
|
+
const header = {
|
|
98
|
+
typ: "dpop+jwt",
|
|
99
|
+
alg: "ES256",
|
|
100
|
+
jwk: publicJwk,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const payload = {
|
|
104
|
+
htm: method.toUpperCase(),
|
|
105
|
+
htu: canonicalHtu(url),
|
|
106
|
+
iat: now,
|
|
107
|
+
jti,
|
|
108
|
+
|
|
109
|
+
// bindings
|
|
110
|
+
did: deviceId,
|
|
111
|
+
oid: orgId,
|
|
112
|
+
|
|
113
|
+
// tone binding (include both for backwards compatibility)
|
|
114
|
+
tsh: await sha256B64Url(tone),
|
|
115
|
+
th: await sha256B64Url(tone),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const enc = new TextEncoder();
|
|
119
|
+
const h = b64url(enc.encode(JSON.stringify(header)));
|
|
120
|
+
const p = b64url(enc.encode(JSON.stringify(payload)));
|
|
121
|
+
const toSign = enc.encode(`${h}.${p}`);
|
|
122
|
+
const s = await signEs256(privateKey, toSign);
|
|
123
|
+
|
|
124
|
+
return `${h}.${p}.${s}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class SidecarClient {
|
|
128
|
+
constructor({
|
|
129
|
+
sidecarBaseUrl,
|
|
130
|
+
orgId,
|
|
131
|
+
userId,
|
|
132
|
+
deviceId,
|
|
133
|
+
sessionId,
|
|
134
|
+
clientIp,
|
|
135
|
+
userAgent = "sidecar-sdk",
|
|
136
|
+
storage,
|
|
137
|
+
}) {
|
|
138
|
+
this.sidecarBaseUrl = sidecarBaseUrl.replace(/\/+$/, "");
|
|
139
|
+
this.storage = storage;
|
|
140
|
+
this.store = normalizeStore(this.storage);
|
|
141
|
+
|
|
142
|
+
this.baseHeaders = {
|
|
143
|
+
"X-Org-Id": orgId,
|
|
144
|
+
"X-User-Id": userId,
|
|
145
|
+
"X-Device-Id": deviceId,
|
|
146
|
+
"X-Session-Id": sessionId,
|
|
147
|
+
"X-Client-Ip": clientIp,
|
|
148
|
+
"User-Agent": userAgent,
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
this.tone = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async _callProxy({ targetUrl, method, headers = {}, body = null, extraHeaders = {} }) {
|
|
156
|
+
const payload = {
|
|
157
|
+
target_url: targetUrl,
|
|
158
|
+
method,
|
|
159
|
+
headers,
|
|
160
|
+
body,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const res = await fetch(`${this.sidecarBaseUrl}/proxy/http`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: { ...this.baseHeaders, ...extraHeaders },
|
|
166
|
+
body: JSON.stringify(payload),
|
|
167
|
+
mode: "cors",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const text = await res.text();
|
|
171
|
+
let json = null;
|
|
172
|
+
try {
|
|
173
|
+
json = text ? JSON.parse(text) : null;
|
|
174
|
+
} catch {
|
|
175
|
+
json = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
ok: res.ok,
|
|
180
|
+
status: res.status,
|
|
181
|
+
json,
|
|
182
|
+
raw: text,
|
|
183
|
+
headers: res.headers,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async fetchThroughSidecar({ targetUrl, method, headers = {}, body = null }) {
|
|
188
|
+
const orgId = this.baseHeaders["X-Org-Id"];
|
|
189
|
+
const deviceId = this.baseHeaders["X-Device-Id"];
|
|
190
|
+
|
|
191
|
+
// 1) If no tone yet, do one probe to obtain it (expect 409 + tone)
|
|
192
|
+
if (!this.tone) {
|
|
193
|
+
const probe = await this._callProxy({ targetUrl, method, headers, body });
|
|
194
|
+
const tone = probe?.json?.tone;
|
|
195
|
+
if (probe.status === 409 && tone) {
|
|
196
|
+
this.tone = tone;
|
|
197
|
+
} else {
|
|
198
|
+
// If proxy didn't return tone, return probe as-is
|
|
199
|
+
return probe;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 2) Attempt real request with tone + DPoP
|
|
204
|
+
const proof1 = await makeDpopProof({
|
|
205
|
+
storage: this.storage,
|
|
206
|
+
method,
|
|
207
|
+
url: targetUrl,
|
|
208
|
+
tone: this.tone,
|
|
209
|
+
deviceId,
|
|
210
|
+
orgId,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
let r = await this._callProxy({
|
|
214
|
+
targetUrl,
|
|
215
|
+
method,
|
|
216
|
+
headers,
|
|
217
|
+
body,
|
|
218
|
+
extraHeaders: { "X-Tone": this.tone, "DPoP": proof1 },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 3) If tone rotated/invalid, retry once with new tone
|
|
222
|
+
const rotated = r.status === 409 && r?.json?.tone;
|
|
223
|
+
if (rotated) {
|
|
224
|
+
this.tone = r.json.tone;
|
|
225
|
+
|
|
226
|
+
const proof2 = await makeDpopProof({
|
|
227
|
+
storage: this.storage,
|
|
228
|
+
method,
|
|
229
|
+
url: targetUrl,
|
|
230
|
+
tone: this.tone,
|
|
231
|
+
deviceId,
|
|
232
|
+
orgId,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
r = await this._callProxy({
|
|
236
|
+
targetUrl,
|
|
237
|
+
method,
|
|
238
|
+
headers,
|
|
239
|
+
body,
|
|
240
|
+
extraHeaders: { "X-Tone": this.tone, "DPoP": proof2 },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return r;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// sdk/src/sidecar_sdk_node.js
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { SidecarClient as CoreSidecarClient } from "./sidecar_sdk_core.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Persistent JSON file store for Node.
|
|
10
|
+
* Keeps the same DPoP key across runs (required for Level 3 binding).
|
|
11
|
+
*/
|
|
12
|
+
export function createFileStore({
|
|
13
|
+
filePath = path.join(os.homedir(), ".cybersidecar", "sdk_store.json"),
|
|
14
|
+
} = {}) {
|
|
15
|
+
const dir = path.dirname(filePath);
|
|
16
|
+
|
|
17
|
+
function readAll() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
20
|
+
return raw ? JSON.parse(raw) : {};
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeAll(obj) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
async get(key) {
|
|
33
|
+
const all = readAll();
|
|
34
|
+
return all[key];
|
|
35
|
+
},
|
|
36
|
+
async set(key, value) {
|
|
37
|
+
const all = readAll();
|
|
38
|
+
all[key] = value;
|
|
39
|
+
writeAll(all);
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
async del(key) {
|
|
43
|
+
const all = readAll();
|
|
44
|
+
delete all[key];
|
|
45
|
+
writeAll(all);
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Node-friendly SidecarClient:
|
|
53
|
+
* - Re-exports the core client
|
|
54
|
+
* - Injects persistent storage by default
|
|
55
|
+
*/
|
|
56
|
+
export class SidecarClient extends CoreSidecarClient {
|
|
57
|
+
constructor(opts = {}) {
|
|
58
|
+
const storage = opts.storage ?? createFileStore();
|
|
59
|
+
super({ ...opts, storage });
|
|
60
|
+
}
|
|
61
|
+
}
|