@dcasegr/skein 0.0.0-stage → 0.1.2
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 +525 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +782 -0
- package/dist/host/router.js +2411 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// BRC-169 §7.3 envelopes — the dag-cbor form (issue #33, #21; BRC-231 bodies):
|
|
2
|
+
// the §7.2 members with identity key, contentHash and signature as byte
|
|
3
|
+
// strings and `content` the BRC-78 bytes; the signature over SHA-256 of the
|
|
4
|
+
// dag-cbor of the map without `content` and `signature` (no canonicalisation
|
|
5
|
+
// step: dag-cbor is deterministic). Same signing key as §7.2 (skein's: [2,
|
|
6
|
+
// "metanet handles envelope"], key "send", counterparty anyone). A signature is
|
|
7
|
+
// over the encoding it was made in, so a relay delivers an envelope in the
|
|
8
|
+
// encoding it was submitted in and never transcodes; the kernel keeps each in
|
|
9
|
+
// its own form (the SDK's lib/message.zig). A message's id is the CID of its
|
|
10
|
+
// signed part as kept.
|
|
11
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
12
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
13
|
+
import { brc78Decode, brc78Encode, ENVELOPE_KEY_ID, ENVELOPE_PROTOCOL, isEnvelope, MESSAGE_ENCRYPTION, signedPart, verify } from "./envelope.js";
|
|
14
|
+
import { verifyAnyone } from "./runtime/identity.js";
|
|
15
|
+
import { encode } from "./runtime/cid.js";
|
|
16
|
+
import { CID } from "multiformats/cid";
|
|
17
|
+
const hex = (b) => Buffer.from(b).toString("hex");
|
|
18
|
+
const isBytes = (x) => x instanceof Uint8Array;
|
|
19
|
+
export function isCborSigned(x) {
|
|
20
|
+
const e = x;
|
|
21
|
+
return !!e && typeof e === "object" && e.metanetHandles === "1.0" && !!e.recipient && typeof e.recipient.handle === "string" && typeof e.recipient.domain === "string"
|
|
22
|
+
&& !!e.sender && isBytes(e.sender.identityKey) && e.sender.identityKey.length === 33 && typeof e.created === "string"
|
|
23
|
+
&& isBytes(e.contentHash) && e.contentHash.length === 32 && isBytes(e.signature);
|
|
24
|
+
}
|
|
25
|
+
export const isCborEnvelope = (x) => isCborSigned(x) && isBytes(x.content);
|
|
26
|
+
/** The preimage: dag-cbor of the envelope without content and signature. */
|
|
27
|
+
export function preimage(env) {
|
|
28
|
+
const { content: _c, signature: _s, ...rest } = env;
|
|
29
|
+
return dagCbor.encode(strip(rest));
|
|
30
|
+
}
|
|
31
|
+
export function cborSignedPart(env) {
|
|
32
|
+
const { content: _c, ...rest } = env;
|
|
33
|
+
return rest;
|
|
34
|
+
}
|
|
35
|
+
export function verifyCbor(env) {
|
|
36
|
+
if (!isCborSigned(env))
|
|
37
|
+
return false;
|
|
38
|
+
const key = hex(env.sender.identityKey);
|
|
39
|
+
const c = env.content;
|
|
40
|
+
if (c !== undefined) {
|
|
41
|
+
try {
|
|
42
|
+
if (brc78Decode(c).sender !== key)
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return verifyAnyone(key, ENVELOPE_PROTOCOL, ENVELOPE_KEY_ID, preimage(env), env.signature);
|
|
50
|
+
}
|
|
51
|
+
/** Seal `body` (dag-cbor) to `recipient` in the §7.3 form. */
|
|
52
|
+
export async function sealCbor(wallet, args) {
|
|
53
|
+
const { publicKey: me } = await wallet.getPublicKey({ identityKey: true });
|
|
54
|
+
const unsigned = strip({
|
|
55
|
+
metanetHandles: "1.0",
|
|
56
|
+
recipient: { handle: args.recipient.handle, domain: args.recipient.domain },
|
|
57
|
+
sender: { identityKey: Uint8Array.from(Buffer.from(me, "hex")), handle: args.sender?.handle, domain: args.sender?.domain },
|
|
58
|
+
created: args.created ?? new Date().toISOString(),
|
|
59
|
+
contentHash: Uint8Array.from(createHash("sha256").update(args.body).digest()),
|
|
60
|
+
});
|
|
61
|
+
const hash = [...createHash("sha256").update(preimage(unsigned)).digest()];
|
|
62
|
+
const { signature } = await wallet.createSignature({ hashToDirectlySign: hash, protocolID: ENVELOPE_PROTOCOL, keyID: ENVELOPE_KEY_ID, counterparty: "anyone" });
|
|
63
|
+
const keyId = new Uint8Array(randomBytes(32));
|
|
64
|
+
const { ciphertext } = await wallet.encrypt({ protocolID: MESSAGE_ENCRYPTION, keyID: Buffer.from(keyId).toString("base64"), counterparty: args.recipient.identityKey, plaintext: [...args.body] });
|
|
65
|
+
const content = brc78Encode({ sender: me, recipient: args.recipient.identityKey, keyId, ciphertext: Uint8Array.from(ciphertext) });
|
|
66
|
+
return { ...unsigned, content, signature: Uint8Array.from(signature) };
|
|
67
|
+
}
|
|
68
|
+
/** Decrypt a §7.3 envelope's content as its recipient; checked against contentHash. */
|
|
69
|
+
export async function openCbor(wallet, env) {
|
|
70
|
+
const m = brc78Decode(env.content);
|
|
71
|
+
const { publicKey: me } = await wallet.getPublicKey({ identityKey: true });
|
|
72
|
+
if (m.recipient !== me)
|
|
73
|
+
throw new Error(`envelope: addressed to ${m.recipient}, not this wallet (${me})`);
|
|
74
|
+
const { plaintext } = await wallet.decrypt({ protocolID: MESSAGE_ENCRYPTION, keyID: Buffer.from(m.keyId).toString("base64"), counterparty: m.sender, ciphertext: [...m.ciphertext] });
|
|
75
|
+
const body = Uint8Array.from(plaintext);
|
|
76
|
+
if (!Buffer.from(createHash("sha256").update(body).digest()).equals(Buffer.from(env.contentHash)))
|
|
77
|
+
throw new Error("envelope: the content does not match its signed contentHash");
|
|
78
|
+
return { body, senderIdentityKey: m.sender };
|
|
79
|
+
}
|
|
80
|
+
export function formOf(x) {
|
|
81
|
+
if (isCborEnvelope(x))
|
|
82
|
+
return "cbor";
|
|
83
|
+
if (isEnvelope(x))
|
|
84
|
+
return "json";
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** The sender's identity key (hex), the recipient (hex, from BRC-78), whether the signature verifies, the signed part and the message id (its CID as kept). */
|
|
88
|
+
export function inspect(env) {
|
|
89
|
+
if (isCborEnvelope(env)) {
|
|
90
|
+
const signed = cborSignedPart(env);
|
|
91
|
+
return { form: "cbor", sender: hex(env.sender.identityKey), recipient: brc78Decode(env.content).recipient, verified: verifyCbor(env), signed, id: encode(signed).cid };
|
|
92
|
+
}
|
|
93
|
+
const e = env;
|
|
94
|
+
const signed = signedPart(e);
|
|
95
|
+
return { form: "json", sender: e.sender.identityKey, recipient: brc78Decode(Buffer.from(e.content, "base64")).recipient, verified: verify(e), signed, id: encode(signed).cid };
|
|
96
|
+
}
|
|
97
|
+
export function isCompactReply(x) {
|
|
98
|
+
const r = x;
|
|
99
|
+
return !!r && typeof r === "object" && !(r instanceof Uint8Array) && r.type === "reply" && CID.asCID(r.replyTo) !== null && isBytes(r.body);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The record a session reply is kept as — the same entry shape as a full
|
|
103
|
+
* envelope's ({envelope, box, body}), its envelope record carrying the proof:
|
|
104
|
+
* the BRC-104 signature over `payload` (the signed request) with the session
|
|
105
|
+
* nonces; `messageId` is SHA-256 of that payload.
|
|
106
|
+
*/
|
|
107
|
+
export function sessionRecord(r, sender, created, s) {
|
|
108
|
+
return {
|
|
109
|
+
type: "reply", replyTo: r.replyTo, sender: { identityKey: Uint8Array.from(Buffer.from(sender, "hex")) }, created,
|
|
110
|
+
contentHash: Uint8Array.from(createHash("sha256").update(r.body).digest()),
|
|
111
|
+
messageId: Uint8Array.from(createHash("sha256").update(s.payload).digest()),
|
|
112
|
+
session: { payload: s.payload, signature: s.signature, nonce: s.nonce, yourNonce: s.yourNonce },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** How a JSON client carries §7.3 bytes in a BRC-33 JSON body: {"dag-cbor": "<base64>"}. */
|
|
116
|
+
export const wrapCbor = (env) => ({ "dag-cbor": Buffer.from(dagCbor.encode(env)).toString("base64") });
|
|
117
|
+
/**
|
|
118
|
+
* A BRC-33 body as an envelope: §7.3 dag-cbor (its bytes, as BRC-231 carries
|
|
119
|
+
* them, or `{"dag-cbor": "<base64>"}`, how a JSON client carries those bytes,
|
|
120
|
+
* or the decoded map), or §7.2 JSON (the object, or its JSON text).
|
|
121
|
+
*/
|
|
122
|
+
export function asEnvelope(raw) {
|
|
123
|
+
let v = raw;
|
|
124
|
+
try {
|
|
125
|
+
for (let i = 0; i < 2 && typeof v === "string"; i++)
|
|
126
|
+
v = JSON.parse(v);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const wrapped = v && typeof v === "object" && !(v instanceof Uint8Array) ? v["dag-cbor"] : undefined;
|
|
132
|
+
if (typeof wrapped === "string")
|
|
133
|
+
v = Uint8Array.from(Buffer.from(wrapped, "base64"));
|
|
134
|
+
if (v instanceof Uint8Array) {
|
|
135
|
+
const bytes = v;
|
|
136
|
+
try {
|
|
137
|
+
v = dagCbor.decode(bytes);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
try {
|
|
141
|
+
v = JSON.parse(new TextDecoder().decode(bytes));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (isCborEnvelope(v) || isEnvelope(v))
|
|
149
|
+
return v;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
function strip(v) {
|
|
153
|
+
if (v === null || typeof v !== "object" || Array.isArray(v) || v instanceof Uint8Array)
|
|
154
|
+
return v;
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const [k, x] of Object.entries(v))
|
|
157
|
+
if (x !== undefined)
|
|
158
|
+
out[k] = strip(x);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WalletInterface } from "@bsv/sdk";
|
|
2
|
+
import { CID } from "multiformats/cid";
|
|
3
|
+
import { type Envelope, type Signed } from "./runtime/envelope.ts";
|
|
4
|
+
export * from "./runtime/envelope.ts";
|
|
5
|
+
/** A `created` value: ISO 8601 of a time in ms since the epoch. */
|
|
6
|
+
/** An envelope's id: CIDv1 dag-cbor, sha2-256 of the dag-cbor encoded signed part (the envelope without `content`). */
|
|
7
|
+
export declare function envelopeCid(env: Envelope | Signed): Promise<CID>;
|
|
8
|
+
export declare const isoTime: (ms: number) => string;
|
|
9
|
+
export interface SealArgs {
|
|
10
|
+
recipient: {
|
|
11
|
+
identityKey: string;
|
|
12
|
+
handle: string;
|
|
13
|
+
domain: string;
|
|
14
|
+
};
|
|
15
|
+
sender?: {
|
|
16
|
+
handle?: string;
|
|
17
|
+
domain?: string;
|
|
18
|
+
};
|
|
19
|
+
body: Uint8Array;
|
|
20
|
+
created?: string;
|
|
21
|
+
quoteId?: string;
|
|
22
|
+
payment?: unknown;
|
|
23
|
+
}
|
|
24
|
+
/** The signed part for `body`: metadata with its contentHash, signed by the wallet. No encryption. */
|
|
25
|
+
export declare function sign(wallet: WalletInterface, args: SealArgs): Promise<Signed>;
|
|
26
|
+
/** The wire encryption: `body` as BRC-78 content from this wallet to `recipient`, base64. */
|
|
27
|
+
export declare function encryptContent(wallet: WalletInterface, recipient: string, body: Uint8Array): Promise<string>;
|
|
28
|
+
/** sign + encryptContent: the envelope as sent. */
|
|
29
|
+
export declare function seal(wallet: WalletInterface, args: SealArgs): Promise<Envelope>;
|
|
30
|
+
/**
|
|
31
|
+
* Decrypt `content` as its recipient and check it against `contentHash`.
|
|
32
|
+
* Throws if this wallet is not the recipient, the content was altered, or the
|
|
33
|
+
* plaintext is not what the sender signed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function open(wallet: WalletInterface, env: Envelope): Promise<{
|
|
36
|
+
body: Uint8Array;
|
|
37
|
+
senderIdentityKey: string;
|
|
38
|
+
}>;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// BRC-169 §7.2 envelopes, as skein sends and receives them (docs/MESSAGES.md).
|
|
2
|
+
// The wallet operations (sign, encrypt, seal, open) for the client (src/client),
|
|
3
|
+
// the peers and the host, over the
|
|
4
|
+
// pure part in src/runtime/envelope.ts (re-exported here). Outside
|
|
5
|
+
// src/runtime: `seal` draws a random BRC-78 key id and reads the clock for
|
|
6
|
+
// `created`, which the machine never does — an instance's own envelopes are
|
|
7
|
+
// sealed inside the step by the program (programs/envelope), from the step's
|
|
8
|
+
// stamp and random stream, through the wallet import.
|
|
9
|
+
//
|
|
10
|
+
// contentHash hex SHA-256 of the plaintext content (the dag-cbor body): in
|
|
11
|
+
// the signed part, so the sender's signature covers the content
|
|
12
|
+
// and anyone holding the plaintext can check who wrote it.
|
|
13
|
+
// content base64 of a BRC-78 portable encrypted message: version
|
|
14
|
+
// 0x42421033, sender identity (33), recipient identity (33), key id
|
|
15
|
+
// (32, random per message), then the wallet's ciphertext under
|
|
16
|
+
// protocol [2, "message encryption"], keyID base64(key id),
|
|
17
|
+
// counterparty the recipient (AES-256-GCM, IV prepended).
|
|
18
|
+
// signature hex DER, by the sender's wallet under [2, "metanet handles
|
|
19
|
+
// envelope"], key id "1", counterparty anyone, over sha256 of the
|
|
20
|
+
// RFC 8785 canonical envelope without `content` and `signature`.
|
|
21
|
+
// Anyone can derive the key from sender.identityKey (verify()).
|
|
22
|
+
//
|
|
23
|
+
// The encryption is wire-level only: it keeps the messagebox operator out. The
|
|
24
|
+
// recipient keeps the signed part (`Signed`: everything but `content`) and the
|
|
25
|
+
// plaintext; its CID is the message's id (`replyTo` names it).
|
|
26
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
27
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
28
|
+
import { CID } from "multiformats/cid";
|
|
29
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
30
|
+
import { brc78Decode, brc78Encode, canonical, contentHash, ENVELOPE_KEY_ID, ENVELOPE_PROTOCOL, MESSAGE_ENCRYPTION, signedPart } from "./runtime/envelope.js";
|
|
31
|
+
export * from "./runtime/envelope.js";
|
|
32
|
+
// ---------------------------------------------------------------- sign, seal, open
|
|
33
|
+
/** A `created` value: ISO 8601 of a time in ms since the epoch. */
|
|
34
|
+
/** An envelope's id: CIDv1 dag-cbor, sha2-256 of the dag-cbor encoded signed part (the envelope without `content`). */
|
|
35
|
+
export async function envelopeCid(env) {
|
|
36
|
+
return CID.createV1(dagCbor.code, await sha256.digest(dagCbor.encode(signedPart(env))));
|
|
37
|
+
}
|
|
38
|
+
export const isoTime = (ms) => new Date(ms).toISOString();
|
|
39
|
+
/** The signed part for `body`: metadata with its contentHash, signed by the wallet. No encryption. */
|
|
40
|
+
export async function sign(wallet, args) {
|
|
41
|
+
const { publicKey: me } = await wallet.getPublicKey({ identityKey: true });
|
|
42
|
+
const unsigned = strip({
|
|
43
|
+
metanetHandles: "1.0",
|
|
44
|
+
recipient: { handle: args.recipient.handle, domain: args.recipient.domain },
|
|
45
|
+
sender: { identityKey: me, handle: args.sender?.handle, domain: args.sender?.domain },
|
|
46
|
+
created: args.created ?? new Date().toISOString(),
|
|
47
|
+
quoteId: args.quoteId,
|
|
48
|
+
payment: args.payment,
|
|
49
|
+
contentHash: contentHash(args.body),
|
|
50
|
+
});
|
|
51
|
+
const hash = [...createHash("sha256").update(canonical(unsigned)).digest()];
|
|
52
|
+
const { signature } = await wallet.createSignature({ hashToDirectlySign: hash, protocolID: ENVELOPE_PROTOCOL, keyID: ENVELOPE_KEY_ID, counterparty: "anyone" });
|
|
53
|
+
return { ...unsigned, signature: Buffer.from(signature).toString("hex") };
|
|
54
|
+
}
|
|
55
|
+
/** The wire encryption: `body` as BRC-78 content from this wallet to `recipient`, base64. */
|
|
56
|
+
export async function encryptContent(wallet, recipient, body) {
|
|
57
|
+
const { publicKey: me } = await wallet.getPublicKey({ identityKey: true });
|
|
58
|
+
const keyId = new Uint8Array(randomBytes(32));
|
|
59
|
+
const { ciphertext } = await wallet.encrypt({
|
|
60
|
+
protocolID: MESSAGE_ENCRYPTION,
|
|
61
|
+
keyID: Buffer.from(keyId).toString("base64"),
|
|
62
|
+
counterparty: recipient,
|
|
63
|
+
plaintext: [...body],
|
|
64
|
+
});
|
|
65
|
+
return Buffer.from(brc78Encode({ sender: me, recipient, keyId, ciphertext: Uint8Array.from(ciphertext) })).toString("base64");
|
|
66
|
+
}
|
|
67
|
+
/** sign + encryptContent: the envelope as sent. */
|
|
68
|
+
export async function seal(wallet, args) {
|
|
69
|
+
const signed = await sign(wallet, args);
|
|
70
|
+
return { ...signed, content: await encryptContent(wallet, args.recipient.identityKey, args.body) };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decrypt `content` as its recipient and check it against `contentHash`.
|
|
74
|
+
* Throws if this wallet is not the recipient, the content was altered, or the
|
|
75
|
+
* plaintext is not what the sender signed.
|
|
76
|
+
*/
|
|
77
|
+
export async function open(wallet, env) {
|
|
78
|
+
const m = brc78Decode(Buffer.from(env.content, "base64"));
|
|
79
|
+
const { publicKey: me } = await wallet.getPublicKey({ identityKey: true });
|
|
80
|
+
if (m.recipient !== me)
|
|
81
|
+
throw new Error(`envelope: addressed to ${m.recipient}, not this wallet (${me})`);
|
|
82
|
+
const { plaintext } = await wallet.decrypt({
|
|
83
|
+
protocolID: MESSAGE_ENCRYPTION,
|
|
84
|
+
keyID: Buffer.from(m.keyId).toString("base64"),
|
|
85
|
+
counterparty: m.sender,
|
|
86
|
+
ciphertext: [...m.ciphertext],
|
|
87
|
+
});
|
|
88
|
+
const body = Uint8Array.from(plaintext);
|
|
89
|
+
if (contentHash(body) !== env.contentHash)
|
|
90
|
+
throw new Error("envelope: the content does not match its signed contentHash");
|
|
91
|
+
return { body, senderIdentityKey: m.sender };
|
|
92
|
+
}
|
|
93
|
+
function strip(v) {
|
|
94
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
95
|
+
return v;
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [k, x] of Object.entries(v))
|
|
98
|
+
if (x !== undefined)
|
|
99
|
+
out[k] = strip(x);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { txCid } from "./feeds.ts";
|
|
2
|
+
import type { HostDb } from "./instances.ts";
|
|
3
|
+
/** The host's Arcade (host config: SKEIN_ARC_* in the environment or $SKEIN_HOME/host.env). */
|
|
4
|
+
export interface ArcConfig {
|
|
5
|
+
/** Arcade's API, e.g. https://arcade.example.com (its POST /tx). */
|
|
6
|
+
url: string;
|
|
7
|
+
/** The host's one callback token: X-CallbackToken on every submission, the SSE stream's scope, the webhook's bearer. */
|
|
8
|
+
token: string;
|
|
9
|
+
/** Arcade's SSE service (its own port): default <url>/events. */
|
|
10
|
+
events?: string;
|
|
11
|
+
/** Where Arcade posts webhooks: a URL of this router's POST /arc/callback that Arcade can reach (Arcade wants public HTTPS). Unset: SSE only. */
|
|
12
|
+
callbackUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The webhook's path on the router. */
|
|
15
|
+
export declare const ARC_ROUTE = "/arc";
|
|
16
|
+
/** The host's Arcade from SKEIN_ARC_URL, SKEIN_ARC_TOKEN, SKEIN_ARC_EVENTS_URL, SKEIN_ARC_CALLBACK_URL (the environment, else $SKEIN_HOME/host.env); none without a URL. */
|
|
17
|
+
export declare function hostArcConfig(vars: Record<string, string | undefined>, home?: string): ArcConfig | undefined;
|
|
18
|
+
export interface ArcAnswer {
|
|
19
|
+
status: number;
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
body: Uint8Array;
|
|
22
|
+
}
|
|
23
|
+
export interface BroadcasterOptions {
|
|
24
|
+
arc: ArcConfig;
|
|
25
|
+
db: HostDb;
|
|
26
|
+
/** Admit a proof event into `handle`'s box (Router.admitEvent): unsigned, self-validating. */
|
|
27
|
+
admit(handle: string, box: string, event: Record<string, unknown>): Promise<unknown>;
|
|
28
|
+
/** Send `handle` a status message from the host's status provider (providers.ts `status`): box `chain/status` (#128), `subject` the transaction's CID. */
|
|
29
|
+
status(handle: string, body: Record<string, unknown>, subject: ReturnType<typeof txCid>): Promise<unknown>;
|
|
30
|
+
/** The instances a transaction may be in: every enabled row, and those whose kernel runs now. */
|
|
31
|
+
instances(): {
|
|
32
|
+
all: string[];
|
|
33
|
+
running: string[];
|
|
34
|
+
};
|
|
35
|
+
/** Whether `handle`'s state holds the transaction (a read), hydrating it if it is not running. */
|
|
36
|
+
holds(handle: string, txid: string): Promise<boolean>;
|
|
37
|
+
log?(source: string, line: string): void;
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
/** SSE reconnect backoff, ms (default 500 → 30 000). */
|
|
40
|
+
backoff?: {
|
|
41
|
+
min: number;
|
|
42
|
+
max: number;
|
|
43
|
+
};
|
|
44
|
+
/** A queued broadcast Arcade did not take: tried again after `min` ms, doubling to `max` (default 1 000 → 300 000). */
|
|
45
|
+
retry?: {
|
|
46
|
+
min: number;
|
|
47
|
+
max: number;
|
|
48
|
+
};
|
|
49
|
+
/** A queued broadcast is dropped this long after it was queued (default a day: the instance abandons it by then). */
|
|
50
|
+
giveUpMs?: number;
|
|
51
|
+
/** How long a call to Arcade may take (default 30 000 ms). */
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
/** Transactions whose holders are cached (default 100 000; the oldest go first). */
|
|
54
|
+
maxKnown?: number;
|
|
55
|
+
/** The clock (ms): the queue's. */
|
|
56
|
+
now?(): number;
|
|
57
|
+
}
|
|
58
|
+
/** The transaction a broadcast body carries (Atomic BEEF, BEEF, raw), and the bytes to post to Arcade: Extended Format when its inputs' sources are there. */
|
|
59
|
+
export declare function arcadeBody(body: Uint8Array): {
|
|
60
|
+
txid?: string;
|
|
61
|
+
bytes: Uint8Array;
|
|
62
|
+
};
|
|
63
|
+
/** A status with a merkle path as the proof event an instance records (#65), or undefined. */
|
|
64
|
+
export declare function proofOf(ev: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
65
|
+
/** A status without a path as the status provider's message body (#65). */
|
|
66
|
+
export declare function statusBodyOf(ev: Record<string, unknown>): Record<string, unknown>;
|
|
67
|
+
export declare class Broadcaster {
|
|
68
|
+
readonly o: BroadcasterOptions;
|
|
69
|
+
/** txid → the instances known to hold it, and whether every enabled instance was asked. */
|
|
70
|
+
private known;
|
|
71
|
+
/** Statuses being routed now (txid + key): a webhook and the stream delivering the same one route it once. */
|
|
72
|
+
private inflight;
|
|
73
|
+
private stream?;
|
|
74
|
+
private timer?;
|
|
75
|
+
private pumping?;
|
|
76
|
+
private again;
|
|
77
|
+
private stopped;
|
|
78
|
+
/** Counts (tests, the log): statuses routed, redeliveries dropped, admissions (proofs and status messages), posts. */
|
|
79
|
+
readonly stats: {
|
|
80
|
+
routed: number;
|
|
81
|
+
duplicates: number;
|
|
82
|
+
admitted: number;
|
|
83
|
+
proofs: number;
|
|
84
|
+
statuses: number;
|
|
85
|
+
posted: number;
|
|
86
|
+
};
|
|
87
|
+
constructor(o: BroadcasterOptions);
|
|
88
|
+
private say;
|
|
89
|
+
private now;
|
|
90
|
+
/** Arcade's SSE service for the host's token. */
|
|
91
|
+
eventsUrl(): string;
|
|
92
|
+
/** Open the subscription (resuming after the last event id taken), and take up the queue where it was. */
|
|
93
|
+
start(): void;
|
|
94
|
+
stop(): Promise<void>;
|
|
95
|
+
/** `handle` holds `txid` (it broadcast it). */
|
|
96
|
+
note(txid: string, handle: string): void;
|
|
97
|
+
/** The instances holding `txid`: the cached ones, and those a read finds (every enabled one the first time, then the running ones not known yet). */
|
|
98
|
+
holders(txid: string): Promise<string[]>;
|
|
99
|
+
/**
|
|
100
|
+
* One status from Arcade (the stream's, a webhook's, or its answer to a
|
|
101
|
+
* post): routed to every instance holding the transaction, once — a proof
|
|
102
|
+
* as an event, anything else as a status message. `askers` (a post's
|
|
103
|
+
* broadcasters) get it even when it was routed before: Arcade's answer to
|
|
104
|
+
* a duplicate is the news for an instance that broadcast it later. The
|
|
105
|
+
* number of instances it reached.
|
|
106
|
+
*/
|
|
107
|
+
deliver(v: unknown, via: "stream" | "webhook" | "post", askers?: string[]): Promise<number>;
|
|
108
|
+
/**
|
|
109
|
+
* A broadcast event from `handle` (#65): the transaction's bytes and, if
|
|
110
|
+
* the event carried it, its Atomic BEEF. Queued durably and posted; the
|
|
111
|
+
* txid, or undefined when the bytes are not a transaction.
|
|
112
|
+
*/
|
|
113
|
+
enqueue(handle: string, tx: Uint8Array, beef?: Uint8Array): string | undefined;
|
|
114
|
+
/** Post what is due now, then wait for the next. */
|
|
115
|
+
private kick;
|
|
116
|
+
private pump;
|
|
117
|
+
private post;
|
|
118
|
+
/** Whether a post (and the routing of its answer) is under way. */
|
|
119
|
+
busy(): boolean;
|
|
120
|
+
/** Until the posts under way, and the routing of their answers, are done (tests, the corpus: Router.settled). */
|
|
121
|
+
idle(): Promise<void>;
|
|
122
|
+
/** The queue as it stands (tests, the log). */
|
|
123
|
+
queued(): Array<{
|
|
124
|
+
txid: string;
|
|
125
|
+
attempts: number;
|
|
126
|
+
next: number;
|
|
127
|
+
}>;
|
|
128
|
+
private arcade;
|
|
129
|
+
/** POST /arc/callback: Arcade's webhook, with the host token as bearer; answered once the status is routed (Arcade retries otherwise). */
|
|
130
|
+
callback(headers: Record<string, string | undefined>, raw: Uint8Array): Promise<ArcAnswer>;
|
|
131
|
+
}
|