@book.dev/sdk 1.60.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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/account.d.ts +57 -0
- package/dist/account.js +104 -0
- package/dist/account.js.map +1 -0
- package/dist/ai.d.ts +264 -0
- package/dist/ai.js +36 -0
- package/dist/ai.js.map +1 -0
- package/dist/backup.d.ts +52 -0
- package/dist/backup.js +35 -0
- package/dist/backup.js.map +1 -0
- package/dist/bookFolder.d.ts +44 -0
- package/dist/bookFolder.js +98 -0
- package/dist/bookFolder.js.map +1 -0
- package/dist/bookfile.d.ts +42 -0
- package/dist/bookfile.js +159 -0
- package/dist/bookfile.js.map +1 -0
- package/dist/client.d.ts +279 -0
- package/dist/client.js +502 -0
- package/dist/client.js.map +1 -0
- package/dist/connection.d.ts +20 -0
- package/dist/connection.js +77 -0
- package/dist/connection.js.map +1 -0
- package/dist/content.d.ts +38 -0
- package/dist/content.js +107 -0
- package/dist/content.js.map +1 -0
- package/dist/database.d.ts +687 -0
- package/dist/database.js +1082 -0
- package/dist/database.js.map +1 -0
- package/dist/formula.d.ts +41 -0
- package/dist/formula.js +469 -0
- package/dist/formula.js.map +1 -0
- package/dist/forwarding/challenge.d.ts +35 -0
- package/dist/forwarding/challenge.js +45 -0
- package/dist/forwarding/challenge.js.map +1 -0
- package/dist/forwarding/encoding.d.ts +14 -0
- package/dist/forwarding/encoding.js +54 -0
- package/dist/forwarding/encoding.js.map +1 -0
- package/dist/forwarding/forwardingClient.d.ts +76 -0
- package/dist/forwarding/forwardingClient.js +128 -0
- package/dist/forwarding/forwardingClient.js.map +1 -0
- package/dist/forwarding/index.d.ts +5 -0
- package/dist/forwarding/index.js +14 -0
- package/dist/forwarding/index.js.map +1 -0
- package/dist/forwarding/siteKey.d.ts +12 -0
- package/dist/forwarding/siteKey.js +36 -0
- package/dist/forwarding/siteKey.js.map +1 -0
- package/dist/forwarding/tunnelClient.d.ts +56 -0
- package/dist/forwarding/tunnelClient.js +206 -0
- package/dist/forwarding/tunnelClient.js.map +1 -0
- package/dist/forwarding/tunnelProtocol.d.ts +45 -0
- package/dist/forwarding/tunnelProtocol.js +29 -0
- package/dist/forwarding/tunnelProtocol.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/mtime.d.ts +51 -0
- package/dist/mtime.js +84 -0
- package/dist/mtime.js.map +1 -0
- package/dist/pageProperties.d.ts +80 -0
- package/dist/pageProperties.js +97 -0
- package/dist/pageProperties.js.map +1 -0
- package/dist/plugins.d.ts +110 -0
- package/dist/plugins.js +113 -0
- package/dist/plugins.js.map +1 -0
- package/dist/routes.d.ts +89 -0
- package/dist/routes.js +89 -0
- package/dist/routes.js.map +1 -0
- package/dist/sampleDocument.d.ts +25 -0
- package/dist/sampleDocument.js +67 -0
- package/dist/sampleDocument.js.map +1 -0
- package/dist/suggestions.d.ts +126 -0
- package/dist/suggestions.js +21 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/templates.d.ts +35 -0
- package/dist/templates.js +555 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const ATTACH_MESSAGE_VERSION = "openbook.attach.v1";
|
|
2
|
+
/** Default freshness window for a signed attach (ms). */
|
|
3
|
+
export declare const ATTACH_SKEW_MS: number;
|
|
4
|
+
export interface AttachClaim {
|
|
5
|
+
siteId: string;
|
|
6
|
+
region: string;
|
|
7
|
+
nonce: string;
|
|
8
|
+
/** epoch millis the desktop stamped when signing. */
|
|
9
|
+
ts: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The exact bytes both sides sign/verify. Order and separators are fixed so the
|
|
13
|
+
* desktop and relay never disagree about the message.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildAttachMessage(claim: AttachClaim): string;
|
|
16
|
+
export declare const REATTACH_MESSAGE_VERSION = "openbook.reattach.v1";
|
|
17
|
+
/**
|
|
18
|
+
* The message a desktop signs to reattach to its existing site by proving key
|
|
19
|
+
* possession (keyed on the public key, since the client may know its key before
|
|
20
|
+
* its site id). Distinct from the tunnel-attach message so a signature for one
|
|
21
|
+
* can never be replayed as the other.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildReattachMessage(claim: {
|
|
24
|
+
publicKey: string;
|
|
25
|
+
nonce: string;
|
|
26
|
+
ts: number;
|
|
27
|
+
}): string;
|
|
28
|
+
export declare const RELAY_ATTACH_VERSION = "openbook.relay.v1";
|
|
29
|
+
/** The message a desktop signs against the relay's connection nonce — the relay's
|
|
30
|
+
* own per-connection key proof, on top of verifying the account-issued ticket. */
|
|
31
|
+
export declare function buildRelayAttachMessage(nonce: string): string;
|
|
32
|
+
/** A single-use, high-entropy challenge nonce. */
|
|
33
|
+
export declare function newNonce(): string;
|
|
34
|
+
/** Reject stamps too far from now in either direction (clock skew tolerant). */
|
|
35
|
+
export declare function isFreshTimestamp(ts: number, now?: number, skewMs?: number): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The site-key half of the double-gated tunnel attach.
|
|
2
|
+
//
|
|
3
|
+
// To attach, the desktop must present BOTH a valid account.book.pub principal AND
|
|
4
|
+
// a fresh signature from the site's device private key over a server-issued
|
|
5
|
+
// challenge. The relay mints a nonce (stored in Redis, single-use), the desktop
|
|
6
|
+
// signs the canonical attach message, and the relay verifies it against the Site
|
|
7
|
+
// row's public key. The nonce + timestamp window stop replay.
|
|
8
|
+
import { b64uEncode } from './encoding';
|
|
9
|
+
export const ATTACH_MESSAGE_VERSION = 'openbook.attach.v1';
|
|
10
|
+
/** Default freshness window for a signed attach (ms). */
|
|
11
|
+
export const ATTACH_SKEW_MS = 2 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* The exact bytes both sides sign/verify. Order and separators are fixed so the
|
|
14
|
+
* desktop and relay never disagree about the message.
|
|
15
|
+
*/
|
|
16
|
+
export function buildAttachMessage(claim) {
|
|
17
|
+
return [ATTACH_MESSAGE_VERSION, claim.siteId, claim.region, claim.nonce, claim.ts].join(':');
|
|
18
|
+
}
|
|
19
|
+
export const REATTACH_MESSAGE_VERSION = 'openbook.reattach.v1';
|
|
20
|
+
/**
|
|
21
|
+
* The message a desktop signs to reattach to its existing site by proving key
|
|
22
|
+
* possession (keyed on the public key, since the client may know its key before
|
|
23
|
+
* its site id). Distinct from the tunnel-attach message so a signature for one
|
|
24
|
+
* can never be replayed as the other.
|
|
25
|
+
*/
|
|
26
|
+
export function buildReattachMessage(claim) {
|
|
27
|
+
return [REATTACH_MESSAGE_VERSION, claim.publicKey, claim.nonce, claim.ts].join(':');
|
|
28
|
+
}
|
|
29
|
+
export const RELAY_ATTACH_VERSION = 'openbook.relay.v1';
|
|
30
|
+
/** The message a desktop signs against the relay's connection nonce — the relay's
|
|
31
|
+
* own per-connection key proof, on top of verifying the account-issued ticket. */
|
|
32
|
+
export function buildRelayAttachMessage(nonce) {
|
|
33
|
+
return `${RELAY_ATTACH_VERSION}:${nonce}`;
|
|
34
|
+
}
|
|
35
|
+
/** A single-use, high-entropy challenge nonce. */
|
|
36
|
+
export function newNonce() {
|
|
37
|
+
const bytes = new Uint8Array(18);
|
|
38
|
+
crypto.getRandomValues(bytes);
|
|
39
|
+
return b64uEncode(bytes);
|
|
40
|
+
}
|
|
41
|
+
/** Reject stamps too far from now in either direction (clock skew tolerant). */
|
|
42
|
+
export function isFreshTimestamp(ts, now = Date.now(), skewMs = ATTACH_SKEW_MS) {
|
|
43
|
+
return Number.isFinite(ts) && Math.abs(now - ts) <= skewMs;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=challenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../../src/forwarding/challenge.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,kFAAkF;AAClF,4EAA4E;AAC5E,gFAAgF;AAChF,iFAAiF;AACjF,8DAA8D;AAE9D,OAAO,EAAC,UAAU,EAAC,MAAM,YAAY,CAAC;AAEtC,MAAM,CAAC,MAAM,sBAAsB,GAAG,oBAAoB,CAAC;AAE3D,yDAAyD;AACzD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAU5C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAkB;IACnD,OAAO,CAAC,sBAAsB,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/F,CAAC;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAqD;IACxF,OAAO,CAAC,wBAAwB,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAExD;mFACmF;AACnF,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,OAAO,GAAG,oBAAoB,IAAI,KAAK,EAAE,CAAC;AAC5C,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,QAAQ;IACtB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,EAAU,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE,EAAE,SAAiB,cAAc;IACpG,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,IAAI,MAAM,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function utf8(s: string): Uint8Array<ArrayBuffer>;
|
|
2
|
+
export declare function fromUtf8(bytes: Uint8Array): string;
|
|
3
|
+
/** Base64url-encode bytes (RFC 4648 §5, no padding). */
|
|
4
|
+
export declare function b64uEncode(bytes: Uint8Array): string;
|
|
5
|
+
/** Decode a base64url string back to bytes. Tolerates missing padding. */
|
|
6
|
+
export declare function b64uDecode(s: string): Uint8Array<ArrayBuffer>;
|
|
7
|
+
export declare function b64uEncodeString(s: string): string;
|
|
8
|
+
export declare function b64uDecodeString(s: string): string;
|
|
9
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
10
|
+
/**
|
|
11
|
+
* Constant-time string compare for secrets/MACs. Always walks the full length of
|
|
12
|
+
* the longer input so timing doesn't leak where two values diverge.
|
|
13
|
+
*/
|
|
14
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Small, dependency-free encoding helpers. We avoid Node's `Buffer` so this
|
|
2
|
+
// module runs unchanged on Node, the Vercel edge runtime, and the browser.
|
|
3
|
+
const enc = new TextEncoder();
|
|
4
|
+
const dec = new TextDecoder();
|
|
5
|
+
// Returns an ArrayBuffer-backed view (not SharedArrayBuffer) so the bytes satisfy
|
|
6
|
+
// Web Crypto's `BufferSource` under TS 6's stricter typed-array generics.
|
|
7
|
+
export function utf8(s) {
|
|
8
|
+
const out = new Uint8Array(s.length * 3);
|
|
9
|
+
const { written } = enc.encodeInto(s, out);
|
|
10
|
+
return out.subarray(0, written);
|
|
11
|
+
}
|
|
12
|
+
export function fromUtf8(bytes) {
|
|
13
|
+
return dec.decode(bytes);
|
|
14
|
+
}
|
|
15
|
+
/** Base64url-encode bytes (RFC 4648 §5, no padding). */
|
|
16
|
+
export function b64uEncode(bytes) {
|
|
17
|
+
let bin = '';
|
|
18
|
+
for (const byte of bytes)
|
|
19
|
+
bin += String.fromCharCode(byte);
|
|
20
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
21
|
+
}
|
|
22
|
+
/** Decode a base64url string back to bytes. Tolerates missing padding. */
|
|
23
|
+
export function b64uDecode(s) {
|
|
24
|
+
const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((s.length + 3) % 4);
|
|
25
|
+
const bin = atob(b64);
|
|
26
|
+
const out = new Uint8Array(bin.length);
|
|
27
|
+
for (let i = 0; i < bin.length; i++)
|
|
28
|
+
out[i] = bin.charCodeAt(i);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
export function b64uEncodeString(s) {
|
|
32
|
+
return b64uEncode(utf8(s));
|
|
33
|
+
}
|
|
34
|
+
export function b64uDecodeString(s) {
|
|
35
|
+
return fromUtf8(b64uDecode(s));
|
|
36
|
+
}
|
|
37
|
+
export function bytesToHex(bytes) {
|
|
38
|
+
let hex = '';
|
|
39
|
+
for (const b of bytes)
|
|
40
|
+
hex += b.toString(16).padStart(2, '0');
|
|
41
|
+
return hex;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Constant-time string compare for secrets/MACs. Always walks the full length of
|
|
45
|
+
* the longer input so timing doesn't leak where two values diverge.
|
|
46
|
+
*/
|
|
47
|
+
export function timingSafeEqual(a, b) {
|
|
48
|
+
const max = Math.max(a.length, b.length);
|
|
49
|
+
let diff = a.length ^ b.length;
|
|
50
|
+
for (let i = 0; i < max; i++)
|
|
51
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
52
|
+
return diff === 0;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.js","sourceRoot":"","sources":["../../src/forwarding/encoding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type FetchLike } from '../client';
|
|
2
|
+
import { type TunnelStatus } from './tunnelClient';
|
|
3
|
+
export interface SiteIdentity {
|
|
4
|
+
siteId: string;
|
|
5
|
+
prefix: string;
|
|
6
|
+
host: string;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
/** base64url PKCS#8 — the secret. Persist ONLY in the OS keychain. */
|
|
9
|
+
privateKey: string;
|
|
10
|
+
}
|
|
11
|
+
/** Where the site identity (incl. private key) is persisted. The desktop backs
|
|
12
|
+
* this with the OS keychain; tests can use MemoryKeyStore. */
|
|
13
|
+
export interface KeyStore {
|
|
14
|
+
load(): Promise<SiteIdentity | null>;
|
|
15
|
+
save(identity: SiteIdentity): Promise<void>;
|
|
16
|
+
clear(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare class MemoryKeyStore implements KeyStore {
|
|
19
|
+
private identity;
|
|
20
|
+
load(): Promise<SiteIdentity | null>;
|
|
21
|
+
save(identity: SiteIdentity): Promise<void>;
|
|
22
|
+
clear(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface ForwardingClientOptions {
|
|
25
|
+
/** https://account.book.pub */
|
|
26
|
+
accountUrl: string;
|
|
27
|
+
/** A device bearer token for the account API (the desktop already holds one). */
|
|
28
|
+
authToken: string;
|
|
29
|
+
keyStore: KeyStore;
|
|
30
|
+
/** Local OpenBook data-server origin to forward, e.g. http://127.0.0.1:4317, or
|
|
31
|
+
* '' when {@link localFetchImpl} resolves paths itself (the desktop IPC transport). */
|
|
32
|
+
localOrigin: string;
|
|
33
|
+
/** The cell to attach in (nearest region). Defaults to the platform home cell. */
|
|
34
|
+
region?: string;
|
|
35
|
+
onStatus?: (status: TunnelStatus) => void;
|
|
36
|
+
/** `fetch` for the account API (account.book.pub). Defaults to the global fetch. */
|
|
37
|
+
fetchImpl?: FetchLike;
|
|
38
|
+
/**
|
|
39
|
+
* `fetch` the tunnel uses to serve inbound requests against the local origin.
|
|
40
|
+
* Distinct from {@link fetchImpl} so the desktop can route the account API to
|
|
41
|
+
* account.book.pub (global fetch) while the tunnel forwards to the *portless*
|
|
42
|
+
* local server over its IPC transport. Defaults to {@link fetchImpl}.
|
|
43
|
+
*/
|
|
44
|
+
localFetchImpl?: FetchLike;
|
|
45
|
+
webSocketImpl?: typeof WebSocket;
|
|
46
|
+
}
|
|
47
|
+
export declare class ForwardingClient {
|
|
48
|
+
private readonly opts;
|
|
49
|
+
private tunnel?;
|
|
50
|
+
private identity?;
|
|
51
|
+
private readonly fetchImpl;
|
|
52
|
+
constructor(opts: ForwardingClientOptions);
|
|
53
|
+
get site(): SiteIdentity | undefined;
|
|
54
|
+
private get region();
|
|
55
|
+
private api;
|
|
56
|
+
/** Reattach to our existing site (if we hold its key), else provision a new one. */
|
|
57
|
+
ensureSite(): Promise<SiteIdentity>;
|
|
58
|
+
private provision;
|
|
59
|
+
private challenge;
|
|
60
|
+
private reattach;
|
|
61
|
+
/**
|
|
62
|
+
* Mint a fresh attach ticket + relay WS URL. Run once per (re)connection: the
|
|
63
|
+
* ticket is short-lived (≈120s), so the tunnel re-mints every time it dials
|
|
64
|
+
* rather than reusing a stale one (which the relay rejects as expired). The
|
|
65
|
+
* `?site=` query is the relay's Durable-Object routing hint on the WS upgrade
|
|
66
|
+
* (an untrusted hint — the relay still verifies the ticket + site-key signature
|
|
67
|
+
* after connecting); omitting it makes the upgrade fail with 400 "missing site".
|
|
68
|
+
*/
|
|
69
|
+
private mintAttach;
|
|
70
|
+
/** Begin forwarding the local instance. Resolves with the public host. */
|
|
71
|
+
start(): Promise<{
|
|
72
|
+
host: string;
|
|
73
|
+
}>;
|
|
74
|
+
/** Stop forwarding (the site stays registered; reconnect later with the same key). */
|
|
75
|
+
stop(): void;
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The desktop-side orchestrator — a thin client over the key-provisioning and
|
|
2
|
+
// relay APIs. The Tauri app wraps this: it supplies a keychain-backed KeyStore and
|
|
3
|
+
// the local OpenBook origin, and gets back a one-call "forward this instance".
|
|
4
|
+
//
|
|
5
|
+
// Flow:
|
|
6
|
+
// ensureSite() → reattach if we already hold a site key, else provision a new one
|
|
7
|
+
// (private key is returned ONCE and handed to the KeyStore).
|
|
8
|
+
// start() → challenge + sign + attach-ticket, then open the tunnel.
|
|
9
|
+
//
|
|
10
|
+
// Runtime-agnostic (fetch + WebSocket globals); no Node-only APIs, so it runs in
|
|
11
|
+
// the Tauri webview or a sidecar alike.
|
|
12
|
+
import { globalFetch } from '../client';
|
|
13
|
+
import { buildAttachMessage, buildReattachMessage } from './challenge';
|
|
14
|
+
import { signWithSiteKey } from './siteKey';
|
|
15
|
+
import { TunnelClient } from './tunnelClient';
|
|
16
|
+
export class MemoryKeyStore {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.identity = null;
|
|
19
|
+
}
|
|
20
|
+
async load() {
|
|
21
|
+
return this.identity;
|
|
22
|
+
}
|
|
23
|
+
async save(identity) {
|
|
24
|
+
this.identity = identity;
|
|
25
|
+
}
|
|
26
|
+
async clear() {
|
|
27
|
+
this.identity = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ForwardingClient {
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.opts = opts;
|
|
33
|
+
this.fetchImpl = opts.fetchImpl ?? globalFetch;
|
|
34
|
+
}
|
|
35
|
+
get site() {
|
|
36
|
+
return this.identity;
|
|
37
|
+
}
|
|
38
|
+
get region() {
|
|
39
|
+
return this.opts.region ?? 'sin1';
|
|
40
|
+
}
|
|
41
|
+
async api(path, body) {
|
|
42
|
+
const res = await this.fetchImpl(`${this.opts.accountUrl.replace(/\/$/, '')}${path}`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { authorization: `Bearer ${this.opts.authToken}`, 'content-type': 'application/json' },
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw new Error(`${path} → ${res.status}`);
|
|
49
|
+
return (await res.json());
|
|
50
|
+
}
|
|
51
|
+
/** Reattach to our existing site (if we hold its key), else provision a new one. */
|
|
52
|
+
async ensureSite() {
|
|
53
|
+
if (this.identity)
|
|
54
|
+
return this.identity;
|
|
55
|
+
const stored = await this.opts.keyStore.load();
|
|
56
|
+
if (stored && (await this.reattach(stored))) {
|
|
57
|
+
this.identity = stored;
|
|
58
|
+
return stored;
|
|
59
|
+
}
|
|
60
|
+
const provisioned = await this.provision();
|
|
61
|
+
await this.opts.keyStore.save(provisioned);
|
|
62
|
+
this.identity = provisioned;
|
|
63
|
+
return provisioned;
|
|
64
|
+
}
|
|
65
|
+
async provision() {
|
|
66
|
+
const data = await this.api('/api/sites', {});
|
|
67
|
+
return {
|
|
68
|
+
siteId: data.site.id,
|
|
69
|
+
prefix: data.site.prefix,
|
|
70
|
+
host: data.site.host,
|
|
71
|
+
publicKey: data.site.publicKey,
|
|
72
|
+
privateKey: data.privateKey,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async challenge(publicKey) {
|
|
76
|
+
return this.api('/api/sites/challenge', { publicKey });
|
|
77
|
+
}
|
|
78
|
+
async reattach(id) {
|
|
79
|
+
try {
|
|
80
|
+
const { nonce, ts } = await this.challenge(id.publicKey);
|
|
81
|
+
const signature = await signWithSiteKey(id.privateKey, buildReattachMessage({ publicKey: id.publicKey, nonce, ts }));
|
|
82
|
+
await this.api('/api/sites/reattach', { publicKey: id.publicKey, nonce, ts, signature });
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false; // key unknown/rotated → caller re-provisions
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mint a fresh attach ticket + relay WS URL. Run once per (re)connection: the
|
|
91
|
+
* ticket is short-lived (≈120s), so the tunnel re-mints every time it dials
|
|
92
|
+
* rather than reusing a stale one (which the relay rejects as expired). The
|
|
93
|
+
* `?site=` query is the relay's Durable-Object routing hint on the WS upgrade
|
|
94
|
+
* (an untrusted hint — the relay still verifies the ticket + site-key signature
|
|
95
|
+
* after connecting); omitting it makes the upgrade fail with 400 "missing site".
|
|
96
|
+
*/
|
|
97
|
+
async mintAttach(id) {
|
|
98
|
+
const { nonce, ts } = await this.challenge(id.publicKey);
|
|
99
|
+
const signature = await signWithSiteKey(id.privateKey, buildAttachMessage({ siteId: id.siteId, region: this.region, nonce, ts }));
|
|
100
|
+
const res = await this.api('/api/sites/attach-ticket', { siteId: id.siteId, nonce, ts, signature, region: this.region });
|
|
101
|
+
return {
|
|
102
|
+
relayWsUrl: `${res.relayBase.replace(/\/$/, '')}/__tunnel?site=${encodeURIComponent(id.siteId)}`,
|
|
103
|
+
ticket: res.ticket,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Begin forwarding the local instance. Resolves with the public host. */
|
|
107
|
+
async start() {
|
|
108
|
+
const id = await this.ensureSite();
|
|
109
|
+
this.tunnel = new TunnelClient({
|
|
110
|
+
ticketProvider: () => this.mintAttach(id), // fresh ticket per (re)connect
|
|
111
|
+
privateKey: id.privateKey,
|
|
112
|
+
localOrigin: this.opts.localOrigin,
|
|
113
|
+
onStatus: this.opts.onStatus,
|
|
114
|
+
// The tunnel forwards to the LOCAL server; the desktop routes that over IPC
|
|
115
|
+
// (no port), separate from the account API's global fetch.
|
|
116
|
+
fetchImpl: this.opts.localFetchImpl ?? this.opts.fetchImpl,
|
|
117
|
+
webSocketImpl: this.opts.webSocketImpl,
|
|
118
|
+
});
|
|
119
|
+
this.tunnel.start();
|
|
120
|
+
return { host: id.host };
|
|
121
|
+
}
|
|
122
|
+
/** Stop forwarding (the site stays registered; reconnect later with the same key). */
|
|
123
|
+
stop() {
|
|
124
|
+
this.tunnel?.stop();
|
|
125
|
+
this.tunnel = undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=forwardingClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forwardingClient.js","sourceRoot":"","sources":["../../src/forwarding/forwardingClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ForwardingClient, MemoryKeyStore, type KeyStore, type SiteIdentity, type ForwardingClientOptions } from './forwardingClient';
|
|
2
|
+
export { TunnelClient, type TunnelStatus, type TunnelClientOptions } from './tunnelClient';
|
|
3
|
+
export { mintSiteKeypair, signWithSiteKey, verifyWithSiteKey, type SiteKeypair } from './siteKey';
|
|
4
|
+
export { buildAttachMessage, buildReattachMessage, buildRelayAttachMessage, newNonce, isFreshTimestamp, type AttachClaim, } from './challenge';
|
|
5
|
+
export { encodeControl, decodeControl, encodeBody, decodeBody, type ControlFrame, } from './tunnelProtocol';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// *.book.pub forwarding — the desktop-side client + shared protocol core.
|
|
2
|
+
//
|
|
3
|
+
// Ported wholesale from open.book.pub (`@book.dev/forwarding` + the relay's
|
|
4
|
+
// ForwardingClient/TunnelClient) so this repo owns the client. open.book.pub
|
|
5
|
+
// keeps only the ForwardingServer (the relay terminator + the account API). The
|
|
6
|
+
// whole surface is runtime-agnostic (Web Crypto + global fetch/WebSocket), so it
|
|
7
|
+
// runs in the Tauri webview; the desktop supplies a keychain-backed KeyStore and
|
|
8
|
+
// the IPC fetch.
|
|
9
|
+
export { ForwardingClient, MemoryKeyStore } from './forwardingClient';
|
|
10
|
+
export { TunnelClient } from './tunnelClient';
|
|
11
|
+
export { mintSiteKeypair, signWithSiteKey, verifyWithSiteKey } from './siteKey';
|
|
12
|
+
export { buildAttachMessage, buildReattachMessage, buildRelayAttachMessage, newNonce, isFreshTimestamp, } from './challenge';
|
|
13
|
+
export { encodeControl, decodeControl, encodeBody, decodeBody, } from './tunnelProtocol';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/forwarding/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,iFAAiF;AACjF,iFAAiF;AACjF,iBAAiB;AAEjB,OAAO,EAAC,gBAAgB,EAAE,cAAc,EAAiE,MAAM,oBAAoB,CAAC;AACpI,OAAO,EAAC,YAAY,EAA8C,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAC,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAmB,MAAM,WAAW,CAAC;AAChG,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,QAAQ,EACR,gBAAgB,GAEjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,GAEX,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SiteKeypair {
|
|
2
|
+
/** Raw 32-byte public key, base64url. Persisted on the Site row; prefix derives from it. */
|
|
3
|
+
publicKey: string;
|
|
4
|
+
/** PKCS#8 private key, base64url. Returned to the desktop ONCE, never stored server-side. */
|
|
5
|
+
privateKey: string;
|
|
6
|
+
}
|
|
7
|
+
/** Mint a fresh per-site Ed25519 keypair. */
|
|
8
|
+
export declare function mintSiteKeypair(): Promise<SiteKeypair>;
|
|
9
|
+
/** Sign an attach challenge with the site's private key (desktop side). */
|
|
10
|
+
export declare function signWithSiteKey(privateKey: string, message: string): Promise<string>;
|
|
11
|
+
/** Verify an attach signature against the site's public key (relay / account side). */
|
|
12
|
+
export declare function verifyWithSiteKey(publicKey: string, message: string, signature: string): Promise<boolean>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Per-site identity keypair (Ed25519). On first connect account.book.pub mints
|
|
2
|
+
// one; the desktop keeps the PRIVATE key (OS keychain), the public key is stored
|
|
3
|
+
// on the Site row and the prefix is derived from it. Every reconnect re-proves
|
|
4
|
+
// possession of the private key to reattach to the same site.
|
|
5
|
+
//
|
|
6
|
+
// Implemented on Web Crypto (`crypto.subtle`) so the same code mints in the
|
|
7
|
+
// account Node runtime and verifies in the relay Node runtime. Ed25519 is native
|
|
8
|
+
// in Node 20+/22 and modern browsers; the edge never runs these (it only does
|
|
9
|
+
// HMAC — see principal verification in open.book.pub).
|
|
10
|
+
import { b64uDecode, b64uEncode, utf8 } from './encoding';
|
|
11
|
+
const ED25519 = { name: 'Ed25519' };
|
|
12
|
+
/** Mint a fresh per-site Ed25519 keypair. */
|
|
13
|
+
export async function mintSiteKeypair() {
|
|
14
|
+
const kp = (await crypto.subtle.generateKey(ED25519, true, ['sign', 'verify']));
|
|
15
|
+
const pub = new Uint8Array(await crypto.subtle.exportKey('raw', kp.publicKey));
|
|
16
|
+
const priv = new Uint8Array(await crypto.subtle.exportKey('pkcs8', kp.privateKey));
|
|
17
|
+
return { publicKey: b64uEncode(pub), privateKey: b64uEncode(priv) };
|
|
18
|
+
}
|
|
19
|
+
/** Sign an attach challenge with the site's private key (desktop side). */
|
|
20
|
+
export async function signWithSiteKey(privateKey, message) {
|
|
21
|
+
const key = await crypto.subtle.importKey('pkcs8', b64uDecode(privateKey), ED25519, false, ['sign']);
|
|
22
|
+
const sig = await crypto.subtle.sign(ED25519, key, utf8(message));
|
|
23
|
+
return b64uEncode(new Uint8Array(sig));
|
|
24
|
+
}
|
|
25
|
+
/** Verify an attach signature against the site's public key (relay / account side). */
|
|
26
|
+
export async function verifyWithSiteKey(publicKey, message, signature) {
|
|
27
|
+
try {
|
|
28
|
+
const key = await crypto.subtle.importKey('raw', b64uDecode(publicKey), ED25519, false, ['verify']);
|
|
29
|
+
return await crypto.subtle.verify(ED25519, key, b64uDecode(signature), utf8(message));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Malformed key/signature → treat as a failed proof, never throw into the gate.
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=siteKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"siteKey.js","sourceRoot":"","sources":["../../src/forwarding/siteKey.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,iFAAiF;AACjF,+EAA+E;AAC/E,8DAA8D;AAC9D,EAAE;AACF,4EAA4E;AAC5E,iFAAiF;AACjF,8EAA8E;AAC9E,uDAAuD;AAEvD,OAAO,EAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAC,MAAM,YAAY,CAAC;AAExD,MAAM,OAAO,GAAG,EAAC,IAAI,EAAE,SAAS,EAAU,CAAC;AAS3C,6CAA6C;AAC7C,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,EAAE,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAkB,CAAC;IACjG,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/E,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IACnF,OAAO,EAAC,SAAS,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC,EAAC,CAAC;AACpE,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,UAAkB,EAAE,OAAe;IACvE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACrG,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAClE,OAAO,UAAU,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAAiB,EACjB,OAAe,EACf,SAAiB;IAEjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpG,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,gFAAgF;QAChF,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type FetchLike } from '../client';
|
|
2
|
+
export type TunnelStatus = 'connecting' | 'online' | 'reconnecting' | 'offline';
|
|
3
|
+
export interface TunnelClientOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Mint a FRESH relay URL + attach ticket for each (re)connection. Tickets are
|
|
6
|
+
* short-lived (≈120s), so the tunnel must obtain a new one every time it dials
|
|
7
|
+
* — reusing a single ticket across a reconnect (or a slow first connect) makes
|
|
8
|
+
* the relay reject the attach as expired ("attach failed"), and the tunnel
|
|
9
|
+
* then loops forever on the same dead ticket. The provider re-runs the account
|
|
10
|
+
* challenge → attach-ticket flow; its `relayWsUrl` must include the `?site=`
|
|
11
|
+
* routing hint the relay needs on the WS upgrade.
|
|
12
|
+
*/
|
|
13
|
+
ticketProvider: () => Promise<{
|
|
14
|
+
relayWsUrl: string;
|
|
15
|
+
ticket: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** The site's private key (base64url PKCS#8) from the OS keychain. */
|
|
18
|
+
privateKey: string;
|
|
19
|
+
/** The local OpenBook data server origin, e.g. http://127.0.0.1:4317 (or '' when
|
|
20
|
+
* `fetchImpl` resolves paths itself, as the desktop IPC transport does). */
|
|
21
|
+
localOrigin: string;
|
|
22
|
+
onStatus?: (status: TunnelStatus) => void;
|
|
23
|
+
fetchImpl?: FetchLike;
|
|
24
|
+
webSocketImpl?: typeof WebSocket;
|
|
25
|
+
maxBackoffMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare class TunnelClient {
|
|
28
|
+
private readonly opts;
|
|
29
|
+
private ws?;
|
|
30
|
+
private status;
|
|
31
|
+
private backoff;
|
|
32
|
+
private stopped;
|
|
33
|
+
private readonly inflight;
|
|
34
|
+
private readonly fetchImpl;
|
|
35
|
+
private readonly WS;
|
|
36
|
+
/** The ticket for the current connection, minted just before the WS opened. */
|
|
37
|
+
private ticket?;
|
|
38
|
+
constructor(opts: TunnelClientOptions);
|
|
39
|
+
start(): void;
|
|
40
|
+
stop(): void;
|
|
41
|
+
get currentStatus(): TunnelStatus;
|
|
42
|
+
private setStatus;
|
|
43
|
+
private connect;
|
|
44
|
+
/** Mint a fresh ticket, then open the relay socket with it. Minting per dial is
|
|
45
|
+
* what keeps reconnects working — a reused ticket expires and attach fails. */
|
|
46
|
+
private dial;
|
|
47
|
+
private onClose;
|
|
48
|
+
private scheduleReconnect;
|
|
49
|
+
private sendControl;
|
|
50
|
+
/** Pause while the socket's send buffer is backed up, so streaming a large
|
|
51
|
+
* response to a slow viewer can't grow memory without bound. */
|
|
52
|
+
private drain;
|
|
53
|
+
private onMessage;
|
|
54
|
+
private onControl;
|
|
55
|
+
private handleRequest;
|
|
56
|
+
}
|