convex-rust 0.0.0-stage → 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/LICENSE +201 -0
- package/README.md +137 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +200 -0
- package/dist/cli/build.d.ts +146 -0
- package/dist/cli/build.js +902 -0
- package/dist/cli/demangle.d.ts +7 -0
- package/dist/cli/demangle.js +328 -0
- package/dist/cli/diagnostics.d.ts +34 -0
- package/dist/cli/diagnostics.js +296 -0
- package/dist/cli/generate.d.ts +29 -0
- package/dist/cli/generate.js +82 -0
- package/dist/cli/manifest.d.ts +24 -0
- package/dist/cli/manifest.js +75 -0
- package/dist/cli/rustApi.d.ts +78 -0
- package/dist/cli/rustApi.js +609 -0
- package/dist/cli/scaffold.d.ts +22 -0
- package/dist/cli/scaffold.js +114 -0
- package/dist/cli/shim.d.ts +30 -0
- package/dist/cli/shim.js +140 -0
- package/dist/cli/size.d.ts +17 -0
- package/dist/cli/size.js +73 -0
- package/dist/cli/split.d.ts +20 -0
- package/dist/cli/split.js +140 -0
- package/dist/cli/symbols.d.ts +30 -0
- package/dist/cli/symbols.js +133 -0
- package/dist/cli/toolchain.d.ts +53 -0
- package/dist/cli/toolchain.js +167 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.js +66 -0
- package/dist/cli/wasisdk.d.ts +82 -0
- package/dist/cli/wasisdk.js +220 -0
- package/dist/cli/wasmHints.d.ts +29 -0
- package/dist/cli/wasmHints.js +172 -0
- package/dist/runtime/base64.d.ts +4 -0
- package/dist/runtime/base64.js +77 -0
- package/dist/runtime/crypto.d.ts +7 -0
- package/dist/runtime/crypto.js +421 -0
- package/dist/runtime/fastjson.d.ts +3 -0
- package/dist/runtime/fastjson.js +170 -0
- package/dist/runtime/host.d.ts +73 -0
- package/dist/runtime/host.js +1230 -0
- package/dist/runtime/http.d.ts +40 -0
- package/dist/runtime/http.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/intl.d.ts +5 -0
- package/dist/runtime/intl.js +69 -0
- package/dist/runtime/tmpfs.d.ts +129 -0
- package/dist/runtime/tmpfs.js +624 -0
- package/dist/runtime/wasi.d.ts +33 -0
- package/dist/runtime/wasi.js +216 -0
- package/package.json +57 -4
- package/rust/convex_fn/Cargo.toml +32 -0
- package/rust/convex_fn/LICENSE +201 -0
- package/rust/convex_fn/build.rs +3 -0
- package/rust/convex_fn/src/args.rs +416 -0
- package/rust/convex_fn/src/crypto.rs +237 -0
- package/rust/convex_fn/src/ctx.rs +1044 -0
- package/rust/convex_fn/src/dns.rs +356 -0
- package/rust/convex_fn/src/error.rs +179 -0
- package/rust/convex_fn/src/executor.rs +577 -0
- package/rust/convex_fn/src/fanout.rs +106 -0
- package/rust/convex_fn/src/fs.rs +19 -0
- package/rust/convex_fn/src/function_ref.rs +219 -0
- package/rust/convex_fn/src/http.rs +441 -0
- package/rust/convex_fn/src/intl.rs +75 -0
- package/rust/convex_fn/src/lib.rs +242 -0
- package/rust/convex_fn/src/logging.rs +222 -0
- package/rust/convex_fn/src/native.rs +360 -0
- package/rust/convex_fn/src/net.rs +122 -0
- package/rust/convex_fn/src/sys.rs +178 -0
- package/rust/convex_fn/src/values.rs +121 -0
- package/rust/convex_fn/src/wire.rs +579 -0
- package/rust/convex_fn_macros/Cargo.toml +13 -0
- package/rust/convex_fn_macros/LICENSE +201 -0
- package/rust/convex_fn_macros/src/derive.rs +632 -0
- package/rust/convex_fn_macros/src/lib.rs +338 -0
- package/rust/convex_reqwest/Cargo.toml +69 -0
- package/rust/convex_reqwest/LICENSE +201 -0
- package/rust/convex_reqwest/src/blocking.rs +287 -0
- package/rust/convex_reqwest/src/body.rs +152 -0
- package/rust/convex_reqwest/src/client.rs +359 -0
- package/rust/convex_reqwest/src/config_types.rs +150 -0
- package/rust/convex_reqwest/src/error.rs +241 -0
- package/rust/convex_reqwest/src/into_url.rs +57 -0
- package/rust/convex_reqwest/src/lib.rs +42 -0
- package/rust/convex_reqwest/src/multipart.rs +140 -0
- package/rust/convex_reqwest/src/request.rs +358 -0
- package/rust/convex_reqwest/src/response.rs +310 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// Host crypto sync ops (contracts/ABI.md "Optional sync ops", crypto version 2): `crypto/caps`,
|
|
2
|
+
// `crypto/verify`, `crypto/sign`, `crypto/aead`, `crypto/digest`, `crypto/hmac`, on WebCrypto
|
|
3
|
+
// (`crypto.subtle`; async, so the caller's JSPI `Suspending` makes them look synchronous to the guest).
|
|
4
|
+
//
|
|
5
|
+
// Byte arguments are either a base64 string or a guest-memory reference `{ptr, len}` that is read
|
|
6
|
+
// straight out of the guest's linear memory (no copy through JSON/base64). `crypto/aead` transforms
|
|
7
|
+
// its `data` reference in place.
|
|
8
|
+
import { decodeBase64, encodeBase64 } from "./base64.js";
|
|
9
|
+
export const CRYPTO_VERSION = 2;
|
|
10
|
+
const HASHES = {
|
|
11
|
+
"256": { name: "SHA-256", len: 32 },
|
|
12
|
+
"384": { name: "SHA-384", len: 48 },
|
|
13
|
+
"512": { name: "SHA-512", len: 64 },
|
|
14
|
+
};
|
|
15
|
+
const DIGESTS = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"];
|
|
16
|
+
const AEADS = ["A128GCM", "A256GCM"];
|
|
17
|
+
const AEAD_KEY_LEN = { A128GCM: 16, A256GCM: 32 };
|
|
18
|
+
const TAG_LEN = 16;
|
|
19
|
+
const NONCE_LEN = 12;
|
|
20
|
+
/** RSA modulus policy, identical on every host (the wasmtime host's aws-lc-rs limits). */
|
|
21
|
+
const RSA_MIN_BITS = 2048;
|
|
22
|
+
const RSA_MAX_BITS = 8192;
|
|
23
|
+
const SIG_ALGS = {};
|
|
24
|
+
for (const [bits, { name: hash, len }] of Object.entries(HASHES)) {
|
|
25
|
+
SIG_ALGS["RS" + bits] = {
|
|
26
|
+
import: { name: "RSASSA-PKCS1-v1_5", hash },
|
|
27
|
+
op: { name: "RSASSA-PKCS1-v1_5" },
|
|
28
|
+
pub: "jwk",
|
|
29
|
+
rsa: true,
|
|
30
|
+
};
|
|
31
|
+
SIG_ALGS["PS" + bits] = {
|
|
32
|
+
import: { name: "RSA-PSS", hash },
|
|
33
|
+
op: { name: "RSA-PSS", saltLength: len },
|
|
34
|
+
pub: "jwk",
|
|
35
|
+
rsa: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
SIG_ALGS.ES256 = {
|
|
39
|
+
import: { name: "ECDSA", namedCurve: "P-256" },
|
|
40
|
+
op: { name: "ECDSA", hash: "SHA-256" },
|
|
41
|
+
pub: "raw",
|
|
42
|
+
rsa: false,
|
|
43
|
+
};
|
|
44
|
+
SIG_ALGS.ES384 = {
|
|
45
|
+
import: { name: "ECDSA", namedCurve: "P-384" },
|
|
46
|
+
op: { name: "ECDSA", hash: "SHA-384" },
|
|
47
|
+
pub: "raw",
|
|
48
|
+
rsa: false,
|
|
49
|
+
};
|
|
50
|
+
SIG_ALGS.EdDSA = { import: { name: "Ed25519" }, op: { name: "Ed25519" }, pub: "raw", rsa: false };
|
|
51
|
+
const subtleOf = () => globalThis.crypto?.subtle;
|
|
52
|
+
function tlv(der, pos) {
|
|
53
|
+
const tag = der[pos];
|
|
54
|
+
let b = der[pos + 1];
|
|
55
|
+
if (tag === undefined || b === undefined)
|
|
56
|
+
throw new Error("malformed DER");
|
|
57
|
+
let p = pos + 2;
|
|
58
|
+
let len = b;
|
|
59
|
+
if (b >= 0x80) {
|
|
60
|
+
const n = b & 0x7f;
|
|
61
|
+
if (n === 0 || n > 4)
|
|
62
|
+
throw new Error("malformed DER");
|
|
63
|
+
len = 0;
|
|
64
|
+
for (let i = 0; i < n; i++)
|
|
65
|
+
len = len * 256 + (der[p++] ?? 0);
|
|
66
|
+
}
|
|
67
|
+
if (p + len > der.length)
|
|
68
|
+
throw new Error("malformed DER");
|
|
69
|
+
return { tag, start: p, end: p + len };
|
|
70
|
+
}
|
|
71
|
+
/** (modulus, exponent) of a PKCS#1 RSAPublicKey DER. */
|
|
72
|
+
function parseRsaPublicKey(der) {
|
|
73
|
+
const seq = tlv(der, 0);
|
|
74
|
+
if (seq.tag !== 0x30)
|
|
75
|
+
throw new Error("bad RSA key");
|
|
76
|
+
const n = tlv(der, seq.start);
|
|
77
|
+
const e = tlv(der, n.end);
|
|
78
|
+
if (n.tag !== 0x02 || e.tag !== 0x02)
|
|
79
|
+
throw new Error("bad RSA key");
|
|
80
|
+
const strip = (t) => {
|
|
81
|
+
let v = der.subarray(t.start, t.end);
|
|
82
|
+
while (v.length > 1 && v[0] === 0)
|
|
83
|
+
v = v.subarray(1);
|
|
84
|
+
return v;
|
|
85
|
+
};
|
|
86
|
+
return { n: strip(n), e: strip(e) };
|
|
87
|
+
}
|
|
88
|
+
function bitLength(v) {
|
|
89
|
+
return v.length === 0 ? 0 : (v.length - 1) * 8 + (32 - Math.clz32(v[0]));
|
|
90
|
+
}
|
|
91
|
+
/** Modulus bit length of a PKCS#8 PrivateKeyInfo wrapping an RSAPrivateKey. */
|
|
92
|
+
function pkcs8RsaBits(der) {
|
|
93
|
+
const top = tlv(der, 0);
|
|
94
|
+
const version = tlv(der, top.start);
|
|
95
|
+
const algId = tlv(der, version.end);
|
|
96
|
+
const octets = tlv(der, algId.end);
|
|
97
|
+
const inner = tlv(der, octets.start);
|
|
98
|
+
const ver = tlv(der, inner.start);
|
|
99
|
+
const n = tlv(der, ver.end);
|
|
100
|
+
let v = der.subarray(n.start, n.end);
|
|
101
|
+
while (v.length > 1 && v[0] === 0)
|
|
102
|
+
v = v.subarray(1);
|
|
103
|
+
return bitLength(v);
|
|
104
|
+
}
|
|
105
|
+
function b64url(b) {
|
|
106
|
+
return encodeBase64(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
107
|
+
}
|
|
108
|
+
function bytes(mem, v, field) {
|
|
109
|
+
if (typeof v === "string")
|
|
110
|
+
return decodeBase64(v);
|
|
111
|
+
if (v && typeof v === "object" && Number.isInteger(v.ptr) && Number.isInteger(v.len)) {
|
|
112
|
+
const buf = mem().buffer;
|
|
113
|
+
const ptr = v.ptr >>> 0;
|
|
114
|
+
const len = v.len >>> 0;
|
|
115
|
+
if (ptr + len > buf.byteLength)
|
|
116
|
+
throw new Error(`${field}: out of bounds`);
|
|
117
|
+
return new Uint8Array(buf, ptr, len);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`missing ${field}`);
|
|
120
|
+
}
|
|
121
|
+
function latin1(b) {
|
|
122
|
+
let s = "";
|
|
123
|
+
for (let i = 0; i < b.length; i += 8192)
|
|
124
|
+
s += String.fromCharCode(...b.subarray(i, i + 8192));
|
|
125
|
+
return s;
|
|
126
|
+
}
|
|
127
|
+
// ---- key cache (per isolate; keys are immutable and small) ----
|
|
128
|
+
const keys = new Map();
|
|
129
|
+
function cached(id, make) {
|
|
130
|
+
let k = keys.get(id);
|
|
131
|
+
if (!k) {
|
|
132
|
+
if (keys.size >= 256)
|
|
133
|
+
keys.clear();
|
|
134
|
+
k = make();
|
|
135
|
+
k.catch(() => keys.delete(id)); // do not cache failures
|
|
136
|
+
keys.set(id, k);
|
|
137
|
+
}
|
|
138
|
+
return k;
|
|
139
|
+
}
|
|
140
|
+
// ---- caps ----
|
|
141
|
+
let caps;
|
|
142
|
+
// RFC 8032 section 7.1 test 1 public key: a valid point, unlike an arbitrary 32-byte string.
|
|
143
|
+
const ED25519_PROBE_KEY = new Uint8Array([
|
|
144
|
+
0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a,
|
|
145
|
+
0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a,
|
|
146
|
+
]);
|
|
147
|
+
function probe() {
|
|
148
|
+
return (caps ??= (async () => {
|
|
149
|
+
const subtle = subtleOf();
|
|
150
|
+
if (!subtle)
|
|
151
|
+
return { error: { message: "crypto.subtle unavailable" } };
|
|
152
|
+
const sigs = Object.keys(SIG_ALGS);
|
|
153
|
+
try {
|
|
154
|
+
// EdDSA is the only family not guaranteed everywhere; prove it before advertising it.
|
|
155
|
+
const key = await subtle.importKey("raw", ED25519_PROBE_KEY, { name: "Ed25519" }, false, ["verify"]);
|
|
156
|
+
await subtle.verify({ name: "Ed25519" }, key, new Uint8Array(64), new Uint8Array(1));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
sigs.splice(sigs.indexOf("EdDSA"), 1);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
ok: {
|
|
163
|
+
version: CRYPTO_VERSION,
|
|
164
|
+
ops: {
|
|
165
|
+
verify: sigs,
|
|
166
|
+
sign: sigs,
|
|
167
|
+
pubkey: sigs.filter((a) => !SIG_ALGS[a].rsa),
|
|
168
|
+
aead: AEADS,
|
|
169
|
+
digest: DIGESTS,
|
|
170
|
+
hmac: DIGESTS,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
})());
|
|
175
|
+
}
|
|
176
|
+
// ---- ops ----
|
|
177
|
+
function sigSpec(op, alg) {
|
|
178
|
+
const spec = typeof alg === "string" && Object.hasOwn(SIG_ALGS, alg) ? SIG_ALGS[alg] : undefined;
|
|
179
|
+
if (!spec)
|
|
180
|
+
throw new Error(`unsupported alg ${alg}`);
|
|
181
|
+
return spec;
|
|
182
|
+
}
|
|
183
|
+
async function verify(args, mem) {
|
|
184
|
+
const spec = sigSpec("verify", args.alg);
|
|
185
|
+
const subtle = subtleOf();
|
|
186
|
+
const keyBytes = bytes(mem, args.key, "key");
|
|
187
|
+
const id = `v:${args.alg}:${latin1(keyBytes)}`;
|
|
188
|
+
const key = await cached(id, () => {
|
|
189
|
+
if (spec.pub === "raw")
|
|
190
|
+
return subtle.importKey("raw", keyBytes.slice(), spec.import, false, ["verify"]);
|
|
191
|
+
const { n, e } = parseRsaPublicKey(keyBytes);
|
|
192
|
+
const bits = bitLength(n);
|
|
193
|
+
if (bits < RSA_MIN_BITS || bits > RSA_MAX_BITS)
|
|
194
|
+
throw new Error(`unsupported RSA key size ${bits}`);
|
|
195
|
+
return subtle.importKey("jwk", { kty: "RSA", n: b64url(n), e: b64url(e), ext: true }, spec.import, false, ["verify"]);
|
|
196
|
+
});
|
|
197
|
+
const ok = await subtle.verify(spec.op, key, bytes(mem, args.sig, "sig"), bytes(mem, args.msg, "msg"));
|
|
198
|
+
return { ok: ok === true };
|
|
199
|
+
}
|
|
200
|
+
async function sign(args, mem) {
|
|
201
|
+
const spec = sigSpec("sign", args.alg);
|
|
202
|
+
const subtle = subtleOf();
|
|
203
|
+
const keyBytes = bytes(mem, args.key, "key");
|
|
204
|
+
const id = `s:${args.alg}:${latin1(keyBytes)}`;
|
|
205
|
+
const key = await cached(id, () => {
|
|
206
|
+
if (spec.rsa) {
|
|
207
|
+
const bits = pkcs8RsaBits(keyBytes);
|
|
208
|
+
if (bits < RSA_MIN_BITS || bits > RSA_MAX_BITS)
|
|
209
|
+
throw new Error(`unsupported RSA key size ${bits}`);
|
|
210
|
+
}
|
|
211
|
+
return subtle.importKey("pkcs8", keyBytes.slice(), spec.import, false, ["sign"]);
|
|
212
|
+
});
|
|
213
|
+
const sig = await subtle.sign(spec.op, key, bytes(mem, args.msg, "msg"));
|
|
214
|
+
return { ok: encodeBase64(new Uint8Array(sig)) };
|
|
215
|
+
}
|
|
216
|
+
function b64urlDecode(s) {
|
|
217
|
+
return decodeBase64(s.replace(/-/g, "+").replace(/_/g, "/"));
|
|
218
|
+
}
|
|
219
|
+
async function pubkey(args, mem) {
|
|
220
|
+
const spec = sigSpec("pubkey", args.alg);
|
|
221
|
+
if (spec.rsa)
|
|
222
|
+
throw new Error(`unsupported alg ${args.alg}`);
|
|
223
|
+
const subtle = subtleOf();
|
|
224
|
+
const keyBytes = bytes(mem, args.key, "key");
|
|
225
|
+
// The private key is imported extractable only here (never cached), to read the public half.
|
|
226
|
+
const key = await subtle.importKey("pkcs8", keyBytes.slice(), spec.import, true, ["sign"]);
|
|
227
|
+
const jwk = await subtle.exportKey("jwk", key);
|
|
228
|
+
if (args.alg === "EdDSA")
|
|
229
|
+
return { ok: encodeBase64(b64urlDecode(jwk.x)) };
|
|
230
|
+
const x = b64urlDecode(jwk.x);
|
|
231
|
+
const y = b64urlDecode(jwk.y);
|
|
232
|
+
const out = new Uint8Array(1 + x.length + y.length);
|
|
233
|
+
out[0] = 4;
|
|
234
|
+
out.set(x, 1);
|
|
235
|
+
out.set(y, 1 + x.length);
|
|
236
|
+
return { ok: encodeBase64(out) };
|
|
237
|
+
}
|
|
238
|
+
async function aead(args, mem) {
|
|
239
|
+
const alg = args.alg;
|
|
240
|
+
if (!AEADS.includes(alg))
|
|
241
|
+
throw new Error(`unsupported alg ${alg}`);
|
|
242
|
+
const dir = args.dir;
|
|
243
|
+
if (dir !== "seal" && dir !== "open")
|
|
244
|
+
throw new Error(`bad dir ${dir}`);
|
|
245
|
+
const subtle = subtleOf();
|
|
246
|
+
const keyBytes = bytes(mem, args.key, "key");
|
|
247
|
+
if (keyBytes.length !== AEAD_KEY_LEN[alg])
|
|
248
|
+
throw new Error(`${alg} needs a ${AEAD_KEY_LEN[alg]}-byte key`);
|
|
249
|
+
const nonce = bytes(mem, args.nonce, "nonce");
|
|
250
|
+
if (nonce.length !== NONCE_LEN)
|
|
251
|
+
throw new Error(`nonce must be ${NONCE_LEN} bytes`);
|
|
252
|
+
const key = await cached(`a:${latin1(keyBytes)}`, () => subtle.importKey("raw", keyBytes.slice(), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]));
|
|
253
|
+
const params = { name: "AES-GCM", iv: nonce, tagLength: TAG_LEN * 8 };
|
|
254
|
+
if (args.aad !== undefined && args.aad !== null) {
|
|
255
|
+
const aad = bytes(mem, args.aad, "aad");
|
|
256
|
+
if (aad.length > 0)
|
|
257
|
+
params.additionalData = aad;
|
|
258
|
+
}
|
|
259
|
+
const data = bytes(mem, args.data, "data");
|
|
260
|
+
if (dir === "seal") {
|
|
261
|
+
const out = new Uint8Array(await subtle.encrypt(params, key, data));
|
|
262
|
+
const n = out.length - TAG_LEN;
|
|
263
|
+
bytes(mem, args.data, "data").set(out.subarray(0, n));
|
|
264
|
+
return { ok: { tag: encodeBase64(out.subarray(n)) } };
|
|
265
|
+
}
|
|
266
|
+
const tag = bytes(mem, args.tag, "tag");
|
|
267
|
+
if (tag.length !== TAG_LEN)
|
|
268
|
+
throw new Error(`tag must be ${TAG_LEN} bytes`);
|
|
269
|
+
const input = new Uint8Array(data.length + TAG_LEN);
|
|
270
|
+
input.set(data);
|
|
271
|
+
input.set(tag, data.length);
|
|
272
|
+
let pt;
|
|
273
|
+
try {
|
|
274
|
+
pt = new Uint8Array(await subtle.decrypt(params, key, input));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return { ok: false }; // authentication failure; `data` is left untouched
|
|
278
|
+
}
|
|
279
|
+
bytes(mem, args.data, "data").set(pt);
|
|
280
|
+
return { ok: true };
|
|
281
|
+
}
|
|
282
|
+
async function digest(args, mem) {
|
|
283
|
+
if (!DIGESTS.includes(args.alg))
|
|
284
|
+
throw new Error(`unsupported alg ${args.alg}`);
|
|
285
|
+
const out = await subtleOf().digest(args.alg, bytes(mem, args.data, "data"));
|
|
286
|
+
return { ok: encodeBase64(new Uint8Array(out)) };
|
|
287
|
+
}
|
|
288
|
+
async function hmac(args, mem) {
|
|
289
|
+
if (!DIGESTS.includes(args.alg))
|
|
290
|
+
throw new Error(`unsupported alg ${args.alg}`);
|
|
291
|
+
const subtle = subtleOf();
|
|
292
|
+
let keyBytes = bytes(mem, args.key, "key");
|
|
293
|
+
// HMAC zero-pads the key to the block size, so an empty key equals one zero byte;
|
|
294
|
+
// WebCrypto rejects zero-length HMAC keys.
|
|
295
|
+
if (keyBytes.length === 0)
|
|
296
|
+
keyBytes = new Uint8Array(1);
|
|
297
|
+
const key = await cached(`h:${args.alg}:${latin1(keyBytes)}`, () => subtle.importKey("raw", keyBytes.slice(), { name: "HMAC", hash: args.alg }, false, ["sign"]));
|
|
298
|
+
const out = await subtle.sign("HMAC", key, bytes(mem, args.data, "data"));
|
|
299
|
+
return { ok: encodeBase64(new Uint8Array(out)) };
|
|
300
|
+
}
|
|
301
|
+
// ---- synchronous SHA-256 / HMAC-SHA256 for small inputs ----
|
|
302
|
+
// A WebCrypto call costs a promise + a JSPI suspend/resume (~15-20 us), which dominates tiny inputs
|
|
303
|
+
// (100k chained HMACs). Inputs up to FAST_MAX bytes are hashed here in plain JS and answered without
|
|
304
|
+
// suspending; larger ones go to crypto.subtle.
|
|
305
|
+
const FAST_MAX = 1024;
|
|
306
|
+
const K256 = new Uint32Array([
|
|
307
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be,
|
|
308
|
+
0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa,
|
|
309
|
+
0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85,
|
|
310
|
+
0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
311
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f,
|
|
312
|
+
0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
313
|
+
]);
|
|
314
|
+
const W256 = new Uint32Array(64);
|
|
315
|
+
function sha256(data) {
|
|
316
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
317
|
+
const n = data.length;
|
|
318
|
+
const total = (n + 9 + 63) & ~63;
|
|
319
|
+
const m = new Uint8Array(total);
|
|
320
|
+
m.set(data);
|
|
321
|
+
m[n] = 0x80;
|
|
322
|
+
const dv = new DataView(m.buffer);
|
|
323
|
+
dv.setUint32(total - 8, Math.floor((n * 8) / 4294967296));
|
|
324
|
+
dv.setUint32(total - 4, (n * 8) >>> 0);
|
|
325
|
+
const w = W256;
|
|
326
|
+
for (let off = 0; off < total; off += 64) {
|
|
327
|
+
for (let i = 0; i < 16; i++)
|
|
328
|
+
w[i] = dv.getUint32(off + i * 4);
|
|
329
|
+
for (let i = 16; i < 64; i++) {
|
|
330
|
+
const a = w[i - 15], b = w[i - 2];
|
|
331
|
+
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
|
332
|
+
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
|
333
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
|
|
334
|
+
}
|
|
335
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
336
|
+
for (let i = 0; i < 64; i++) {
|
|
337
|
+
const t1 = (hh + (((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7))) + ((e & f) ^ (~e & g)) + K256[i] + w[i]) | 0;
|
|
338
|
+
const t2 = ((((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10))) + ((a & b) ^ (a & c) ^ (b & c))) | 0;
|
|
339
|
+
hh = g;
|
|
340
|
+
g = f;
|
|
341
|
+
f = e;
|
|
342
|
+
e = (d + t1) | 0;
|
|
343
|
+
d = c;
|
|
344
|
+
c = b;
|
|
345
|
+
b = a;
|
|
346
|
+
a = (t1 + t2) | 0;
|
|
347
|
+
}
|
|
348
|
+
h[0] += a;
|
|
349
|
+
h[1] += b;
|
|
350
|
+
h[2] += c;
|
|
351
|
+
h[3] += d;
|
|
352
|
+
h[4] += e;
|
|
353
|
+
h[5] += f;
|
|
354
|
+
h[6] += g;
|
|
355
|
+
h[7] += hh;
|
|
356
|
+
}
|
|
357
|
+
const out = new Uint8Array(32);
|
|
358
|
+
const odv = new DataView(out.buffer);
|
|
359
|
+
for (let i = 0; i < 8; i++)
|
|
360
|
+
odv.setUint32(i * 4, h[i]);
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
function hmacSha256(key, data) {
|
|
364
|
+
const k = new Uint8Array(64);
|
|
365
|
+
k.set(key.length > 64 ? sha256(key) : key);
|
|
366
|
+
const inner = new Uint8Array(64 + data.length);
|
|
367
|
+
const outer = new Uint8Array(96);
|
|
368
|
+
for (let i = 0; i < 64; i++) {
|
|
369
|
+
inner[i] = k[i] ^ 0x36;
|
|
370
|
+
outer[i] = k[i] ^ 0x5c;
|
|
371
|
+
}
|
|
372
|
+
inner.set(data, 64);
|
|
373
|
+
outer.set(sha256(inner), 64);
|
|
374
|
+
return sha256(outer);
|
|
375
|
+
}
|
|
376
|
+
/** The synchronous answer for a small SHA-256 digest / HMAC, else undefined (use WebCrypto). */
|
|
377
|
+
function fast(op, args, mem) {
|
|
378
|
+
if (args?.alg !== "SHA-256" || (op !== "crypto/digest" && op !== "crypto/hmac"))
|
|
379
|
+
return undefined;
|
|
380
|
+
try {
|
|
381
|
+
const data = bytes(mem, args.data, "data");
|
|
382
|
+
if (data.length > FAST_MAX)
|
|
383
|
+
return undefined;
|
|
384
|
+
if (op === "crypto/digest")
|
|
385
|
+
return { ok: encodeBase64(sha256(data)) };
|
|
386
|
+
const key = bytes(mem, args.key, "key");
|
|
387
|
+
return key.length > FAST_MAX ? undefined : { ok: encodeBase64(hmacSha256(key, data)) };
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
return { error: { message: `${op}: ${e?.message ?? e}` } };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const OPS = {
|
|
394
|
+
"crypto/verify": verify,
|
|
395
|
+
"crypto/sign": sign,
|
|
396
|
+
"crypto/pubkey": pubkey,
|
|
397
|
+
"crypto/aead": aead,
|
|
398
|
+
"crypto/digest": digest,
|
|
399
|
+
"crypto/hmac": hmac,
|
|
400
|
+
};
|
|
401
|
+
/** Serves one `crypto/*` sync op. `mem` returns the guest's linear memory (for `{ptr, len}` byte arguments). */
|
|
402
|
+
/** Answers synchronously (no suspend) for small SHA-256 digest / HMAC, else with a promise. */
|
|
403
|
+
export function cryptoOp(op, args, mem) {
|
|
404
|
+
const quick = fast(op, args, mem);
|
|
405
|
+
return quick !== undefined ? quick : cryptoAsync(op, args, mem);
|
|
406
|
+
}
|
|
407
|
+
async function cryptoAsync(op, args, mem) {
|
|
408
|
+
if (op === "crypto/caps")
|
|
409
|
+
return probe();
|
|
410
|
+
const f = Object.hasOwn(OPS, op) ? OPS[op] : undefined;
|
|
411
|
+
if (!f)
|
|
412
|
+
return { error: { message: `unknown sync op ${op}` } };
|
|
413
|
+
if (!subtleOf())
|
|
414
|
+
return { error: { message: `${op}: crypto.subtle unavailable` } };
|
|
415
|
+
try {
|
|
416
|
+
return await f(args, mem);
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
return { error: { message: `${op}: ${e?.message ?? e}` } };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Fast Convex-JSON for the host's args / result. Semantically identical to
|
|
2
|
+
// `JSON.stringify(convexToJson(v))` and `jsonToConvex(JSON.parse(t))`: the fast paths only handle the
|
|
3
|
+
// common, valid shapes; anything else (a validation failure, an unsupported type, an odd encoding) is
|
|
4
|
+
// handed to the `convex/values` functions so the result or the error is exactly theirs.
|
|
5
|
+
import { convexToJson, jsonToConvex } from "convex/values";
|
|
6
|
+
import { decodeBase64, encodeBase64 } from "./base64.js";
|
|
7
|
+
const MAX_INT64 = 0x7fffffffffffffffn;
|
|
8
|
+
const MIN_INT64 = -0x8000000000000000n;
|
|
9
|
+
const f64 = new Float64Array(1);
|
|
10
|
+
const f64b = new Uint8Array(f64.buffer);
|
|
11
|
+
const i64 = new BigInt64Array(1);
|
|
12
|
+
const i64b = new Uint8Array(i64.buffer);
|
|
13
|
+
// The typed-array views are in platform byte order; Convex wants little endian.
|
|
14
|
+
const LITTLE = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
|
|
15
|
+
// Valid field names: <= 1024 chars, non-control ASCII, no leading `$`.
|
|
16
|
+
const VALID_FIELD = /^(?:[ -#%-~][ -~]*)?$/;
|
|
17
|
+
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
18
|
+
class Fallback extends Error {
|
|
19
|
+
}
|
|
20
|
+
const FALLBACK = new Fallback();
|
|
21
|
+
function validField(k) {
|
|
22
|
+
return k.length <= 1024 && VALID_FIELD.test(k);
|
|
23
|
+
}
|
|
24
|
+
function replacer(key, v) {
|
|
25
|
+
switch (typeof v) {
|
|
26
|
+
case "string":
|
|
27
|
+
// A `toJSON` (Date) turned an object into a string: not a Convex value.
|
|
28
|
+
if (this[key] !== v)
|
|
29
|
+
throw FALLBACK;
|
|
30
|
+
return v;
|
|
31
|
+
case "boolean":
|
|
32
|
+
return v;
|
|
33
|
+
case "number":
|
|
34
|
+
if (Number.isFinite(v) && !Object.is(v, -0))
|
|
35
|
+
return v;
|
|
36
|
+
f64[0] = v;
|
|
37
|
+
return { $float: encodeBase64(f64b) };
|
|
38
|
+
case "bigint":
|
|
39
|
+
if (v < MIN_INT64 || MAX_INT64 < v)
|
|
40
|
+
throw FALLBACK;
|
|
41
|
+
i64[0] = v;
|
|
42
|
+
return { $integer: encodeBase64(i64b) };
|
|
43
|
+
case "object": {
|
|
44
|
+
if (v === null)
|
|
45
|
+
return v;
|
|
46
|
+
// `toJSON` already ran (Date, the commit-ts placeholder, ...): not a plain Convex value.
|
|
47
|
+
if (this[key] !== v)
|
|
48
|
+
throw FALLBACK;
|
|
49
|
+
if (v instanceof ArrayBuffer)
|
|
50
|
+
return { $bytes: encodeBase64(new Uint8Array(v)) };
|
|
51
|
+
if (Array.isArray(v)) {
|
|
52
|
+
for (let i = 0; i < v.length; i++)
|
|
53
|
+
if (v[i] === undefined)
|
|
54
|
+
throw FALLBACK;
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
const proto = Object.getPrototypeOf(v);
|
|
58
|
+
if (proto !== Object.prototype && proto !== null)
|
|
59
|
+
throw FALLBACK;
|
|
60
|
+
const keys = Object.keys(v);
|
|
61
|
+
let sorted = true;
|
|
62
|
+
for (let i = 0; i < keys.length; i++) {
|
|
63
|
+
const k = keys[i];
|
|
64
|
+
if (v[k] !== undefined && !validField(k))
|
|
65
|
+
throw FALLBACK;
|
|
66
|
+
if (i > 0 && keys[i - 1] > k)
|
|
67
|
+
sorted = false;
|
|
68
|
+
}
|
|
69
|
+
if (sorted)
|
|
70
|
+
return v;
|
|
71
|
+
// convexToJson emits keys sorted by UTF-16 code units; keep that order on the wire.
|
|
72
|
+
keys.sort();
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const k of keys)
|
|
75
|
+
if (v[k] !== undefined)
|
|
76
|
+
out[k] = v[k];
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
// undefined, function, symbol
|
|
81
|
+
throw FALLBACK;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function stringifyConvex(value) {
|
|
85
|
+
if (LITTLE) {
|
|
86
|
+
try {
|
|
87
|
+
const s = JSON.stringify(value, replacer);
|
|
88
|
+
if (typeof s === "string")
|
|
89
|
+
return s;
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// FALLBACK or any error raised while walking: the reference conversion decides.
|
|
93
|
+
void e;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify(convexToJson(value));
|
|
97
|
+
}
|
|
98
|
+
function bytes8(s) {
|
|
99
|
+
if (typeof s !== "string" || s.length !== 12 || !BASE64.test(s))
|
|
100
|
+
return null;
|
|
101
|
+
try {
|
|
102
|
+
const b = decodeBase64(s);
|
|
103
|
+
return b.length === 8 ? b : null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Decodes Convex JSON in place; anything irregular is handed to `jsonToConvex` (same value or same error). */
|
|
110
|
+
function fix(v) {
|
|
111
|
+
if (v === null || typeof v !== "object")
|
|
112
|
+
return v;
|
|
113
|
+
if (Array.isArray(v)) {
|
|
114
|
+
for (let i = 0; i < v.length; i++) {
|
|
115
|
+
const x = v[i];
|
|
116
|
+
if (x !== null && typeof x === "object")
|
|
117
|
+
v[i] = fix(x);
|
|
118
|
+
}
|
|
119
|
+
return v;
|
|
120
|
+
}
|
|
121
|
+
const keys = Object.keys(v);
|
|
122
|
+
if (keys.length === 1 && keys[0].charCodeAt(0) === 36 /* $ */) {
|
|
123
|
+
const k = keys[0];
|
|
124
|
+
const s = v[k];
|
|
125
|
+
if (k === "$bytes" && typeof s === "string" && s.length % 4 === 0 && BASE64.test(s)) {
|
|
126
|
+
try {
|
|
127
|
+
const b = decodeBase64(s);
|
|
128
|
+
// Own, exactly sized buffer (like Base64.toByteArray(..).buffer).
|
|
129
|
+
return b.byteOffset === 0 && b.byteLength === b.buffer.byteLength
|
|
130
|
+
? b.buffer
|
|
131
|
+
: b.slice().buffer;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// fall through to the reference decoder
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (k === "$integer") {
|
|
138
|
+
const b = bytes8(s);
|
|
139
|
+
if (b !== null) {
|
|
140
|
+
i64b.set(b);
|
|
141
|
+
return i64[0];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (k === "$float") {
|
|
145
|
+
const b = bytes8(s);
|
|
146
|
+
if (b !== null) {
|
|
147
|
+
f64b.set(b);
|
|
148
|
+
const f = f64[0];
|
|
149
|
+
if (Number.isNaN(f) || !Number.isFinite(f) || Object.is(f, -0))
|
|
150
|
+
return f;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return jsonToConvex(v);
|
|
154
|
+
}
|
|
155
|
+
for (let i = 0; i < keys.length; i++) {
|
|
156
|
+
const k = keys[i];
|
|
157
|
+
if (!validField(k))
|
|
158
|
+
return jsonToConvex(v); // throws the reference error
|
|
159
|
+
const x = v[k];
|
|
160
|
+
if (x !== null && typeof x === "object")
|
|
161
|
+
v[k] = fix(x);
|
|
162
|
+
}
|
|
163
|
+
return v;
|
|
164
|
+
}
|
|
165
|
+
/** `jsonToConvex(JSON.parse(text))`, converting the freshly parsed tree in place. */
|
|
166
|
+
export function parseConvexValue(parsed) {
|
|
167
|
+
if (!LITTLE)
|
|
168
|
+
return jsonToConvex(parsed);
|
|
169
|
+
return fix(parsed);
|
|
170
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** The slice of the action `ctx` the host uses. */
|
|
2
|
+
export interface HostCtx {
|
|
3
|
+
runQuery(ref: any, args: any): Promise<any>;
|
|
4
|
+
runMutation(ref: any, args: any): Promise<any>;
|
|
5
|
+
runAction(ref: any, args: any): Promise<any>;
|
|
6
|
+
scheduler?: {
|
|
7
|
+
runAt(ts: number, ref: any, args: any): Promise<any>;
|
|
8
|
+
cancel(id: any): Promise<any>;
|
|
9
|
+
};
|
|
10
|
+
auth?: {
|
|
11
|
+
getUserIdentity(): Promise<any>;
|
|
12
|
+
};
|
|
13
|
+
vectorSearch?(table: string, index: string, query: any): Promise<any[]>;
|
|
14
|
+
storage?: {
|
|
15
|
+
generateUploadUrl?(): Promise<string>;
|
|
16
|
+
store(blob: Blob): Promise<string>;
|
|
17
|
+
get(id: any): Promise<Blob | null>;
|
|
18
|
+
getUrl(id: any): Promise<string | null>;
|
|
19
|
+
delete(id: any): Promise<void>;
|
|
20
|
+
getMetadata?(id: any): Promise<any>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare const PANIC_LOG_PREFIX = "Uncaught panic: ";
|
|
24
|
+
type Json = any;
|
|
25
|
+
/**
|
|
26
|
+
* The function argument for ctx.run* / scheduler from a syscall's args, as `getFunctionAddress`
|
|
27
|
+
* in `convex/server` reads it: `{functionHandle}` (a `function://` string), `{reference}` (a
|
|
28
|
+
* component function, carried under `Symbol.for("toReferencePath")`) or `{name}` (a path).
|
|
29
|
+
*/
|
|
30
|
+
export declare function functionRef(syscall: string, args: Json): any;
|
|
31
|
+
/** Rebuilds a vector filter from its wire expressions (`$eq` / `$or`) with the public builder. */
|
|
32
|
+
export declare function vectorFilter(q: any, e: Json): any;
|
|
33
|
+
/**
|
|
34
|
+
* Runs one `convex_run:*` export of a `"use rust"` module (guest ABI v1). `module` is the
|
|
35
|
+
* `WebAssembly.Module` the bundler produced for `import wasm from "./x.wasm"` (compiled once per
|
|
36
|
+
* isolate); raw wasm bytes are also accepted and compiled once.
|
|
37
|
+
*
|
|
38
|
+
* The instance is reused by later calls in the same isolate after a call that completed cleanly
|
|
39
|
+
* (like JS module globals, Rust statics persist). After a trap / panic / OOM, or if another call is
|
|
40
|
+
* still using it, a fresh instance is used. Set the deployment variable `CONVEX_RUST_TRACE=1` to log
|
|
41
|
+
* per-call phase timings.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runRust(ctx: HostCtx, module: WebAssembly.Module | BufferSource, exportName: string, args: unknown, options?: RunRustOptions): Promise<any>;
|
|
44
|
+
/**
|
|
45
|
+
* Runs a `#[http_action]` export of a `"use rust"` module as the handler of a Convex `httpAction`
|
|
46
|
+
* (contracts/V8HOST.md "HTTP actions"). The request head is passed as the call args and its body
|
|
47
|
+
* as a host stream the guest reads incrementally; the guest answers with a buffered response or
|
|
48
|
+
* streams it (`httpStart` / `httpWrite` / `httpEnd` tasks), in which case the returned Response
|
|
49
|
+
* is available as soon as the guest started it and the body follows while the guest runs.
|
|
50
|
+
*
|
|
51
|
+
* Both bodies are limited to 20 MiB, like any Convex HTTP action; a buffered body also has to fit
|
|
52
|
+
* the 16 MiB result limit (stream bigger ones).
|
|
53
|
+
*/
|
|
54
|
+
export declare function runRustHttp(ctx: HostCtx, module: WebAssembly.Module | BufferSource, exportName: string, request: Request, options?: RunRustOptions): Promise<Response>;
|
|
55
|
+
export interface RunRustOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Names of environment variables to expose to the guest (WASI environ). The default runtime's
|
|
58
|
+
* `process.env` can only be read by name, so it cannot be enumerated; `convex-rust build --env A,B`
|
|
59
|
+
* emits this list, and the deployment variable `CONVEX_RUST_ENV=C,D` adds more at call time.
|
|
60
|
+
*/
|
|
61
|
+
env?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Cap, in bytes, on the in-memory `/tmp` the guest sees (per call, discarded after it). Default 64 MiB,
|
|
64
|
+
* or the deployment variable `CONVEX_RUST_TMP_BYTES`; `0` removes `/tmp`. Writes beyond it fail with ENOSPC.
|
|
65
|
+
*/
|
|
66
|
+
tmpBytes?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Environment for the guest: only the named variables (`names`, the build-time `--env` list) plus those
|
|
70
|
+
* listed in the deployment variable `CONVEX_RUST_ENV`. The rest of `process.env` is never enumerated.
|
|
71
|
+
*/
|
|
72
|
+
export declare function collectEnviron(names: string[]): string[];
|
|
73
|
+
export {};
|