@muhkoo/connect 0.6.0-alpha.11 → 0.7.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.js +605 -75
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +316 -10
- package/dist/server/index.js +605 -75
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { prove, verify as verify$1 } from '@zk-kit/groth16';
|
|
2
2
|
import { p256 } from '@noble/curves/nist.js';
|
|
3
3
|
import { buildPoseidon } from 'circomlibjs';
|
|
4
|
+
import { scrypt } from '@noble/hashes/scrypt.js';
|
|
4
5
|
import { entropyToMnemonic, validateMnemonic, mnemonicToEntropy } from '@scure/bip39';
|
|
5
6
|
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
6
|
-
import { scrypt } from '@noble/hashes/scrypt.js';
|
|
7
7
|
import { ristretto255_oprf } from '@noble/curves/ed25519.js';
|
|
8
8
|
|
|
9
9
|
const _isTty = (() => {
|
|
@@ -4303,14 +4303,75 @@ class AuthClient {
|
|
|
4303
4303
|
const res = await this.fetchFn(`${this.baseUrl}/api/auth/oprf`, this.json("POST", { username, blinded }));
|
|
4304
4304
|
return await this.parse("oprfEvaluate", res);
|
|
4305
4305
|
}
|
|
4306
|
-
/**
|
|
4307
|
-
|
|
4308
|
-
|
|
4306
|
+
/**
|
|
4307
|
+
* Gated split-key OPRF evaluation (M2 — email/google factors). Requires a
|
|
4308
|
+
* live `verifyToken` from {@link emailOtpVerify}; returns BOTH trust
|
|
4309
|
+
* domains' evaluations. The recover token is consumed by this call.
|
|
4310
|
+
*/
|
|
4311
|
+
async oprfEvaluateGated(username, blinded, factorType, purpose, verifyToken) {
|
|
4312
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/oprf`, this.json("POST", { username, blinded, factorType, purpose, verifyToken }));
|
|
4313
|
+
return await this.parse("oprfEvaluateGated", res);
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* Request an email OTP. With a session `token` this is the ENROLL flow
|
|
4317
|
+
* (code sent to `email`); without one it's RECOVER (code sent to the
|
|
4318
|
+
* account's enrolled address — the response is uniform either way).
|
|
4319
|
+
*/
|
|
4320
|
+
async emailOtpStart(opts) {
|
|
4321
|
+
const init = this.json("POST", { email: opts.email, username: opts.username });
|
|
4322
|
+
if (opts.token)
|
|
4323
|
+
init.headers["X-Muhkoo-Session"] = opts.token;
|
|
4324
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/email/start`, init);
|
|
4325
|
+
await this.parse("emailOtpStart", res);
|
|
4326
|
+
}
|
|
4327
|
+
/** Trade an emailed code for a single-use verifyToken (+ the proven address). */
|
|
4328
|
+
async emailOtpVerify(opts) {
|
|
4329
|
+
const init = this.json("POST", { code: opts.code, username: opts.username });
|
|
4330
|
+
if (opts.token)
|
|
4331
|
+
init.headers["X-Muhkoo-Session"] = opts.token;
|
|
4332
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/email/verify`, init);
|
|
4333
|
+
return await this.parse("emailOtpVerify", res);
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* Verify a Google ID token (M2.3). With a session `token` it's the ENROLL
|
|
4337
|
+
* gate; without, it's RECOVER/LOGIN (the Google account must already be
|
|
4338
|
+
* linked). Returns a single-use verifyToken bound to the account's `sub`.
|
|
4339
|
+
*/
|
|
4340
|
+
async googleVerify(opts) {
|
|
4341
|
+
const init = this.json("POST", { idToken: opts.idToken, username: opts.username });
|
|
4342
|
+
if (opts.token)
|
|
4343
|
+
init.headers["X-Muhkoo-Session"] = opts.token;
|
|
4344
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/google/verify`, init);
|
|
4345
|
+
return await this.parse("googleVerify", res);
|
|
4346
|
+
}
|
|
4347
|
+
// ---- Hosted-auth authorization-code flow (H0) ---------------------------
|
|
4348
|
+
/** Mint a single-use authorization code for an app (called by the hosted page; session-authed). */
|
|
4349
|
+
async grant(token, body) {
|
|
4350
|
+
const init = this.json("POST", body);
|
|
4351
|
+
init.headers["X-Muhkoo-Session"] = token;
|
|
4352
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/grant`, init);
|
|
4353
|
+
return await this.parse("grant", res);
|
|
4354
|
+
}
|
|
4355
|
+
/** Exchange an authorization code + PKCE verifier for the session + sealed key material. */
|
|
4356
|
+
async token(body) {
|
|
4357
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/token`, this.json("POST", body));
|
|
4358
|
+
return await this.parse("token", res);
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4361
|
+
* Read a factor record for `username` (returns an indistinguishable decoy if
|
|
4362
|
+
* absent). Gated types (email) require the recover `verifyToken`.
|
|
4363
|
+
*/
|
|
4364
|
+
async vaultRead(username, factorType, verifyToken) {
|
|
4365
|
+
const res = await this.fetchFn(`${this.baseUrl}/api/auth/vault`, this.json("POST", { username, factorType, verifyToken }));
|
|
4309
4366
|
return await this.parse("vaultRead", res);
|
|
4310
4367
|
}
|
|
4311
|
-
/**
|
|
4312
|
-
|
|
4313
|
-
|
|
4368
|
+
/**
|
|
4369
|
+
* Add/replace a factor (session-authed; the seed is already wrapped
|
|
4370
|
+
* client-side). Email factors additionally require (and consume) the
|
|
4371
|
+
* enroll `verifyToken`; the stored address must be the verified one.
|
|
4372
|
+
*/
|
|
4373
|
+
async vaultPutFactor(token, factor, verifyToken) {
|
|
4374
|
+
const init = this.json("PUT", { factor, verifyToken });
|
|
4314
4375
|
init.headers["Authorization"] = `Bearer ${token}`;
|
|
4315
4376
|
const res = await this.fetchFn(`${this.baseUrl}/api/auth/vault/factor`, init);
|
|
4316
4377
|
return await this.parse("vaultPutFactor", res);
|
|
@@ -4700,6 +4761,129 @@ async function signMessage(message, privateKey) {
|
|
|
4700
4761
|
return bytesToBase64(new Uint8Array(sig));
|
|
4701
4762
|
}
|
|
4702
4763
|
|
|
4764
|
+
/**
|
|
4765
|
+
* Identity vault primitives (M1.0).
|
|
4766
|
+
*
|
|
4767
|
+
* The vault decouples the identity from the password: a random 32-byte **master
|
|
4768
|
+
* seed** yields the (unchanged) ZK identity via {@link deriveIdentityFromSeed},
|
|
4769
|
+
* and that seed is AES-256-GCM-wrapped under one key *per factor* (password,
|
|
4770
|
+
* passkey, recovery phrase). Server stores only ciphertext.
|
|
4771
|
+
*
|
|
4772
|
+
* This module holds the PURE primitives (seed gen, the memory-hard password
|
|
4773
|
+
* pre-hash, AES-GCM wrap/unwrap, the OPRF -> wrap-key HKDF). The networked
|
|
4774
|
+
* orchestration (calling the server's OPRF endpoint, fetching/storing factor
|
|
4775
|
+
* records) lives in the AuthNamespace, which has transport access.
|
|
4776
|
+
*
|
|
4777
|
+
* Memory-hard KDF: we use **scrypt** (vetted, in `@noble/hashes`, runs in
|
|
4778
|
+
* browser + Workers + Node - no wasm dependency) instead of argon2id; it is the
|
|
4779
|
+
* pre-hash fed into the OPRF, so even if the OPRF key ever leaked, guesses still
|
|
4780
|
+
* pay the scrypt cost.
|
|
4781
|
+
*/
|
|
4782
|
+
const TE$1 = new TextEncoder();
|
|
4783
|
+
// Interactive-login scrypt params (~100ms on a laptop). N must be a power of 2.
|
|
4784
|
+
const SCRYPT = { N: 1 << 15, r: 8, p: 1, dkLen: 32 };
|
|
4785
|
+
/** 32 random bytes - the master seed for a brand-new identity. */
|
|
4786
|
+
function randomSeed() {
|
|
4787
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
4788
|
+
}
|
|
4789
|
+
/** Memory-hard pre-hash of the password - the OPRF input. Salted by username. */
|
|
4790
|
+
function passwordPreHash(username, password) {
|
|
4791
|
+
return scrypt(TE$1.encode(password), TE$1.encode("muhkoo-vault-v1:" + username.toLowerCase()), SCRYPT);
|
|
4792
|
+
}
|
|
4793
|
+
/** HKDF the 64-byte OPRF output into a non-extractable AES-256-GCM wrap key. */
|
|
4794
|
+
async function wrapKeyFromOprf(oprfOutput) {
|
|
4795
|
+
const prk = await crypto.subtle.importKey("raw", oprfOutput, { name: "HKDF" }, false, ["deriveKey"]);
|
|
4796
|
+
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), info: TE$1.encode("muhkoo-vault-wrap") }, prk, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
4797
|
+
}
|
|
4798
|
+
/** HKDF arbitrary 32-byte key material (e.g. a passkey PRF output) into an AES-GCM wrap key. */
|
|
4799
|
+
async function wrapKeyFromBytes(keyMaterial, info) {
|
|
4800
|
+
const prk = await crypto.subtle.importKey("raw", keyMaterial, { name: "HKDF" }, false, ["deriveKey"]);
|
|
4801
|
+
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), info: TE$1.encode(info) }, prk, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
4802
|
+
}
|
|
4803
|
+
async function wrapSeed(seed, key) {
|
|
4804
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
4805
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, seed);
|
|
4806
|
+
return { iv: toBase64(iv), ct: toBase64(new Uint8Array(ct)) };
|
|
4807
|
+
}
|
|
4808
|
+
/** Decrypt a wrapped seed. Throws (AES-GCM tag failure) on a wrong key. */
|
|
4809
|
+
async function unwrapSeed(wrapped, key) {
|
|
4810
|
+
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromBase64(wrapped.iv) }, key, fromBase64(wrapped.ct));
|
|
4811
|
+
return new Uint8Array(pt);
|
|
4812
|
+
}
|
|
4813
|
+
function toBase64(bytes) {
|
|
4814
|
+
let s = "";
|
|
4815
|
+
for (let i = 0; i < bytes.length; i++)
|
|
4816
|
+
s += String.fromCharCode(bytes[i]);
|
|
4817
|
+
return btoa(s);
|
|
4818
|
+
}
|
|
4819
|
+
function fromBase64(b64) {
|
|
4820
|
+
const s = atob(b64);
|
|
4821
|
+
const out = new Uint8Array(s.length);
|
|
4822
|
+
for (let i = 0; i < s.length; i++)
|
|
4823
|
+
out[i] = s.charCodeAt(i);
|
|
4824
|
+
return out;
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
/**
|
|
4828
|
+
* Hosted-auth handoff crypto (auth.muhkoo.dev — AUTH_HOSTED_PLAN.md §3).
|
|
4829
|
+
*
|
|
4830
|
+
* The hosted page authenticates the user and ends up holding the master seed.
|
|
4831
|
+
* To pass it to the developer's app without it ever sitting server-readable or
|
|
4832
|
+
* landing in browser history:
|
|
4833
|
+
*
|
|
4834
|
+
* 1. {@link sealSeed} generates a one-time key K_t, AES-256-GCM-seals the seed
|
|
4835
|
+
* under it, and returns the ciphertext envelope (`sealedKeys`, stored in
|
|
4836
|
+
* the grant the server relays but can't read) plus K_t (`fragmentKey`,
|
|
4837
|
+
* placed ONLY in the redirect URL fragment — fragments never hit a server).
|
|
4838
|
+
* 2. The app exchanges the code for `sealedKeys`, reads K_t from the fragment,
|
|
4839
|
+
* and {@link unsealSeed} recovers the seed, then strips the fragment.
|
|
4840
|
+
*
|
|
4841
|
+
* Trust-equivalent to today's embedded SDK (the app still ends up with the
|
|
4842
|
+
* seed) while moving credential entry onto the Muhkoo origin. The envelope is
|
|
4843
|
+
* versioned so a future per-app-key handoff (v2) can swap in without a protocol
|
|
4844
|
+
* change.
|
|
4845
|
+
*/
|
|
4846
|
+
const TE = new TextEncoder();
|
|
4847
|
+
const TD = new TextDecoder();
|
|
4848
|
+
/** Seal the master seed under a fresh one-time key for the redirect handoff. */
|
|
4849
|
+
async function sealSeed(seed) {
|
|
4850
|
+
const ktRaw = crypto.getRandomValues(new Uint8Array(32));
|
|
4851
|
+
const key = await crypto.subtle.importKey("raw", ktRaw, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
4852
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
4853
|
+
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, seed));
|
|
4854
|
+
const envelope = { v: 1, iv: toBase64(iv), ct: toBase64(ct) };
|
|
4855
|
+
return {
|
|
4856
|
+
sealedKeys: toBase64(TE.encode(JSON.stringify(envelope))),
|
|
4857
|
+
fragmentKey: base64UrlEncode(ktRaw),
|
|
4858
|
+
};
|
|
4859
|
+
}
|
|
4860
|
+
/** Recover the master seed from the sealed envelope + the fragment key. */
|
|
4861
|
+
async function unsealSeed(sealedKeys, fragmentKey) {
|
|
4862
|
+
const envelope = JSON.parse(TD.decode(fromBase64(sealedKeys)));
|
|
4863
|
+
if (envelope.v !== 1)
|
|
4864
|
+
throw new Error(`Unsupported handoff envelope version ${envelope.v}`);
|
|
4865
|
+
const ktRaw = base64UrlDecode(fragmentKey);
|
|
4866
|
+
const key = await crypto.subtle.importKey("raw", ktRaw, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
4867
|
+
const seed = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromBase64(envelope.iv) }, key, fromBase64(envelope.ct));
|
|
4868
|
+
return new Uint8Array(seed);
|
|
4869
|
+
}
|
|
4870
|
+
/** Generate a PKCE verifier + S256 challenge for the authorize→token exchange. */
|
|
4871
|
+
async function generatePkce() {
|
|
4872
|
+
const codeVerifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
4873
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", TE.encode(codeVerifier)));
|
|
4874
|
+
return { codeVerifier, codeChallenge: base64UrlEncode(digest) };
|
|
4875
|
+
}
|
|
4876
|
+
/** A random URL-safe value for the OAuth `state` (CSRF binding on the callback). */
|
|
4877
|
+
function randomState() {
|
|
4878
|
+
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
|
|
4879
|
+
}
|
|
4880
|
+
function base64UrlEncode(bytes) {
|
|
4881
|
+
return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4882
|
+
}
|
|
4883
|
+
function base64UrlDecode(s) {
|
|
4884
|
+
return fromBase64(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "="));
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4703
4887
|
/**
|
|
4704
4888
|
* SDK build version — stamped to the console when a {@link Client} is created.
|
|
4705
4889
|
*
|
|
@@ -4708,7 +4892,7 @@ async function signMessage(message, privateKey) {
|
|
|
4708
4892
|
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
4709
4893
|
* `package.json` `version`.
|
|
4710
4894
|
*/
|
|
4711
|
-
const VERSION = "0.
|
|
4895
|
+
const VERSION = "0.7.0-alpha.3";
|
|
4712
4896
|
|
|
4713
4897
|
/**
|
|
4714
4898
|
* `HttpClient` — the one place the unified {@link Client} talks to the
|
|
@@ -5023,69 +5207,6 @@ function mnemonicToSeed(mnemonic) {
|
|
|
5023
5207
|
return mnemonicToEntropy(m, wordlist);
|
|
5024
5208
|
}
|
|
5025
5209
|
|
|
5026
|
-
/**
|
|
5027
|
-
* Identity vault primitives (M1.0).
|
|
5028
|
-
*
|
|
5029
|
-
* The vault decouples the identity from the password: a random 32-byte **master
|
|
5030
|
-
* seed** yields the (unchanged) ZK identity via {@link deriveIdentityFromSeed},
|
|
5031
|
-
* and that seed is AES-256-GCM-wrapped under one key *per factor* (password,
|
|
5032
|
-
* passkey, recovery phrase). Server stores only ciphertext.
|
|
5033
|
-
*
|
|
5034
|
-
* This module holds the PURE primitives (seed gen, the memory-hard password
|
|
5035
|
-
* pre-hash, AES-GCM wrap/unwrap, the OPRF -> wrap-key HKDF). The networked
|
|
5036
|
-
* orchestration (calling the server's OPRF endpoint, fetching/storing factor
|
|
5037
|
-
* records) lives in the AuthNamespace, which has transport access.
|
|
5038
|
-
*
|
|
5039
|
-
* Memory-hard KDF: we use **scrypt** (vetted, in `@noble/hashes`, runs in
|
|
5040
|
-
* browser + Workers + Node - no wasm dependency) instead of argon2id; it is the
|
|
5041
|
-
* pre-hash fed into the OPRF, so even if the OPRF key ever leaked, guesses still
|
|
5042
|
-
* pay the scrypt cost.
|
|
5043
|
-
*/
|
|
5044
|
-
const TE = new TextEncoder();
|
|
5045
|
-
// Interactive-login scrypt params (~100ms on a laptop). N must be a power of 2.
|
|
5046
|
-
const SCRYPT = { N: 1 << 15, r: 8, p: 1, dkLen: 32 };
|
|
5047
|
-
/** 32 random bytes - the master seed for a brand-new identity. */
|
|
5048
|
-
function randomSeed() {
|
|
5049
|
-
return crypto.getRandomValues(new Uint8Array(32));
|
|
5050
|
-
}
|
|
5051
|
-
/** Memory-hard pre-hash of the password - the OPRF input. Salted by username. */
|
|
5052
|
-
function passwordPreHash(username, password) {
|
|
5053
|
-
return scrypt(TE.encode(password), TE.encode("muhkoo-vault-v1:" + username.toLowerCase()), SCRYPT);
|
|
5054
|
-
}
|
|
5055
|
-
/** HKDF the 64-byte OPRF output into a non-extractable AES-256-GCM wrap key. */
|
|
5056
|
-
async function wrapKeyFromOprf(oprfOutput) {
|
|
5057
|
-
const prk = await crypto.subtle.importKey("raw", oprfOutput, { name: "HKDF" }, false, ["deriveKey"]);
|
|
5058
|
-
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), info: TE.encode("muhkoo-vault-wrap") }, prk, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
5059
|
-
}
|
|
5060
|
-
/** HKDF arbitrary 32-byte key material (e.g. a passkey PRF output) into an AES-GCM wrap key. */
|
|
5061
|
-
async function wrapKeyFromBytes(keyMaterial, info) {
|
|
5062
|
-
const prk = await crypto.subtle.importKey("raw", keyMaterial, { name: "HKDF" }, false, ["deriveKey"]);
|
|
5063
|
-
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), info: TE.encode(info) }, prk, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
5064
|
-
}
|
|
5065
|
-
async function wrapSeed(seed, key) {
|
|
5066
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
5067
|
-
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, seed);
|
|
5068
|
-
return { iv: toBase64(iv), ct: toBase64(new Uint8Array(ct)) };
|
|
5069
|
-
}
|
|
5070
|
-
/** Decrypt a wrapped seed. Throws (AES-GCM tag failure) on a wrong key. */
|
|
5071
|
-
async function unwrapSeed(wrapped, key) {
|
|
5072
|
-
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromBase64(wrapped.iv) }, key, fromBase64(wrapped.ct));
|
|
5073
|
-
return new Uint8Array(pt);
|
|
5074
|
-
}
|
|
5075
|
-
function toBase64(bytes) {
|
|
5076
|
-
let s = "";
|
|
5077
|
-
for (let i = 0; i < bytes.length; i++)
|
|
5078
|
-
s += String.fromCharCode(bytes[i]);
|
|
5079
|
-
return btoa(s);
|
|
5080
|
-
}
|
|
5081
|
-
function fromBase64(b64) {
|
|
5082
|
-
const s = atob(b64);
|
|
5083
|
-
const out = new Uint8Array(s.length);
|
|
5084
|
-
for (let i = 0; i < s.length; i++)
|
|
5085
|
-
out[i] = s.charCodeAt(i);
|
|
5086
|
-
return out;
|
|
5087
|
-
}
|
|
5088
|
-
|
|
5089
5210
|
/**
|
|
5090
5211
|
* OPRF (Oblivious PRF) — RFC 9497, ristretto255-SHA512, mode 0x00.
|
|
5091
5212
|
*
|
|
@@ -5119,6 +5240,55 @@ function oprfFinalize(input, blind, evaluated) {
|
|
|
5119
5240
|
return OPRF.finalize(input, blind, evaluated);
|
|
5120
5241
|
}
|
|
5121
5242
|
|
|
5243
|
+
/**
|
|
5244
|
+
* Verification-gated vault factors (M2 — email, later Google).
|
|
5245
|
+
*
|
|
5246
|
+
* These factors have NO client-side secret: the user proves *channel control*
|
|
5247
|
+
* (an email OTP) and the server releases a split-key OPRF evaluation —
|
|
5248
|
+
* `wrapKey = HKDF( OPRF(K1, input) ‖ OPRF(K2, input) )` where K1 lives on the
|
|
5249
|
+
* accelerator and K2 in a separate trust domain, so no single server-side
|
|
5250
|
+
* compromise can derive the wrap key offline. See accelerator
|
|
5251
|
+
* `AUTH_M2_PLAN.md` §1; the canonical reference implementation is
|
|
5252
|
+
* accelerator `tests/gated-oprf.test.ts` — keep these derivations in
|
|
5253
|
+
* byte-exact lockstep with it.
|
|
5254
|
+
*/
|
|
5255
|
+
/**
|
|
5256
|
+
* Deterministic OPRF input for an email factor. Public-ish by design (the
|
|
5257
|
+
* verification gate, not the input, is what protects the eval) — but it must
|
|
5258
|
+
* be stable forever: username + address are lowercased, the tag is versioned.
|
|
5259
|
+
*/
|
|
5260
|
+
async function emailFactorInput(username, email) {
|
|
5261
|
+
const tag = `muhkoo/factor:email:v1:${username.toLowerCase()}:${email.toLowerCase()}`;
|
|
5262
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(tag));
|
|
5263
|
+
return new Uint8Array(digest);
|
|
5264
|
+
}
|
|
5265
|
+
/**
|
|
5266
|
+
* Deterministic OPRF input for a google factor — keyed on the stable Google
|
|
5267
|
+
* account id (`sub`), not the email (which can change). Same versioning rules
|
|
5268
|
+
* as {@link emailFactorInput}.
|
|
5269
|
+
*/
|
|
5270
|
+
async function googleFactorInput(username, sub) {
|
|
5271
|
+
const tag = `muhkoo/factor:google:v1:${username.toLowerCase()}:${sub}`;
|
|
5272
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(tag));
|
|
5273
|
+
return new Uint8Array(digest);
|
|
5274
|
+
}
|
|
5275
|
+
/** Blind a gated input for the split eval (same blind serves both domains). */
|
|
5276
|
+
function gatedBlind(input) {
|
|
5277
|
+
return oprfBlind(input);
|
|
5278
|
+
}
|
|
5279
|
+
/**
|
|
5280
|
+
* Fold the two domains' evaluations into the AES-256-GCM wrap key:
|
|
5281
|
+
* unblind each, concatenate, HKDF with the gated-factor domain separation.
|
|
5282
|
+
*/
|
|
5283
|
+
async function gatedWrapKey(input, blind, evaluatedB64, evaluated2B64) {
|
|
5284
|
+
const out1 = oprfFinalize(input, blind, fromBase64(evaluatedB64));
|
|
5285
|
+
const out2 = oprfFinalize(input, blind, fromBase64(evaluated2B64));
|
|
5286
|
+
const ikm = new Uint8Array(out1.length + out2.length);
|
|
5287
|
+
ikm.set(out1);
|
|
5288
|
+
ikm.set(out2, out1.length);
|
|
5289
|
+
return wrapKeyFromBytes(ikm, "muhkoo-gated-wrap-v1");
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5122
5292
|
/**
|
|
5123
5293
|
* Passkey factor (M1.1) — WebAuthn with the **PRF extension**.
|
|
5124
5294
|
*
|
|
@@ -5236,6 +5406,147 @@ async function evaluatePasskeyPrf(rpId, credentialId, salt) {
|
|
|
5236
5406
|
return new Uint8Array(results.first);
|
|
5237
5407
|
}
|
|
5238
5408
|
|
|
5409
|
+
/**
|
|
5410
|
+
* `client.auth.hosted` — centralized hosted auth (auth.muhkoo.dev).
|
|
5411
|
+
* AUTH_HOSTED_PLAN.md.
|
|
5412
|
+
*
|
|
5413
|
+
* Two sides share this class:
|
|
5414
|
+
* - **Developer apps** call {@link login} (redirect to the hosted page) and
|
|
5415
|
+
* {@link handleCallback} (exchange the returned code → session + seed). The
|
|
5416
|
+
* app carries no snarkjs and never sees the user's credentials.
|
|
5417
|
+
* - **The hosted page** (the muhkoo/auth SPA), once it has authenticated the
|
|
5418
|
+
* user via the normal `client.auth.zk.*` flow, calls
|
|
5419
|
+
* {@link completeAuthorize} to seal the seed, mint the grant, and build the
|
|
5420
|
+
* redirect-back URL.
|
|
5421
|
+
*
|
|
5422
|
+
* Flow (authorization-code + PKCE + sealed-seed handoff):
|
|
5423
|
+
* app → GET auth.muhkoo.dev/authorize?app_id&redirect_uri&state&code_challenge
|
|
5424
|
+
* page → (user authenticates) → POST /api/auth/grant → redirect_uri?code&state#k=<K_t>
|
|
5425
|
+
* app → POST /api/auth/token {code, code_verifier} → {sessionToken, sealedKeys}
|
|
5426
|
+
* → unseal sealedKeys with K_t → master seed → session ready
|
|
5427
|
+
*/
|
|
5428
|
+
const PKCE_STORE_KEY = "muhkoo.hosted.pkce";
|
|
5429
|
+
class HostedAuth {
|
|
5430
|
+
deps;
|
|
5431
|
+
constructor(deps) {
|
|
5432
|
+
this.deps = deps;
|
|
5433
|
+
}
|
|
5434
|
+
/**
|
|
5435
|
+
* (App side) Begin hosted login: stash a PKCE verifier + state, then send
|
|
5436
|
+
* the browser to the hosted authorize page. Returns the URL it redirects to
|
|
5437
|
+
* (also useful for popup mode / tests). `redirectUri` must be registered for
|
|
5438
|
+
* the app in the portal.
|
|
5439
|
+
*/
|
|
5440
|
+
async login(opts) {
|
|
5441
|
+
const { codeVerifier, codeChallenge } = await generatePkce();
|
|
5442
|
+
const state = randomState();
|
|
5443
|
+
this.store({ codeVerifier, state, appId: opts.appId, redirectUri: opts.redirectUri });
|
|
5444
|
+
const url = `${this.deps.authBaseUrl}/authorize?` + new URLSearchParams({
|
|
5445
|
+
app_id: opts.appId,
|
|
5446
|
+
redirect_uri: opts.redirectUri,
|
|
5447
|
+
state,
|
|
5448
|
+
code_challenge: codeChallenge,
|
|
5449
|
+
code_challenge_method: "S256",
|
|
5450
|
+
}).toString();
|
|
5451
|
+
if (opts.redirect !== false)
|
|
5452
|
+
this.location().assign(url);
|
|
5453
|
+
return url;
|
|
5454
|
+
}
|
|
5455
|
+
/** Whether the current URL looks like a hosted-auth callback (has code + state). */
|
|
5456
|
+
isCallback() {
|
|
5457
|
+
try {
|
|
5458
|
+
const q = new URLSearchParams(this.location().search);
|
|
5459
|
+
return q.has("code") && q.has("state");
|
|
5460
|
+
}
|
|
5461
|
+
catch {
|
|
5462
|
+
return false;
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
/**
|
|
5466
|
+
* (App side) Complete login on the callback URL: verify state, exchange the
|
|
5467
|
+
* code (+ PKCE verifier) for the session, unseal the seed from the fragment,
|
|
5468
|
+
* establish the session, and scrub the URL. Returns the signed-in user.
|
|
5469
|
+
*/
|
|
5470
|
+
async handleCallback() {
|
|
5471
|
+
const loc = this.location();
|
|
5472
|
+
const query = new URLSearchParams(loc.search);
|
|
5473
|
+
const code = query.get("code");
|
|
5474
|
+
const state = query.get("state");
|
|
5475
|
+
const fragmentKey = new URLSearchParams(loc.hash.replace(/^#/, "")).get("k");
|
|
5476
|
+
if (!code || !state)
|
|
5477
|
+
throw new Error("Not a hosted-auth callback.");
|
|
5478
|
+
if (!fragmentKey)
|
|
5479
|
+
throw new Error("Missing handoff key — the login link was altered.");
|
|
5480
|
+
const stash = this.take();
|
|
5481
|
+
if (!stash || stash.state !== state)
|
|
5482
|
+
throw new Error("State mismatch — possible CSRF; start login again.");
|
|
5483
|
+
const res = await this.deps.auth.token({ code, codeVerifier: stash.codeVerifier, appId: stash.appId });
|
|
5484
|
+
const seed = await unsealSeed(res.sealedKeys, fragmentKey);
|
|
5485
|
+
const identity = await deriveIdentityFromSeed(seed);
|
|
5486
|
+
await this.deps.session.setSession({ token: res.sessionToken, username: res.username, commitment: res.commitment });
|
|
5487
|
+
this.deps.session.setIdentity(identity);
|
|
5488
|
+
this.deps.session.setSeed(seed);
|
|
5489
|
+
this.scrubUrl();
|
|
5490
|
+
return { username: res.username, commitment: res.commitment };
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* (Hosted-page side) Finish an /authorize request after the user has
|
|
5494
|
+
* authenticated here: seal the in-memory seed, mint the grant, and return
|
|
5495
|
+
* the URL to redirect the browser back to. The seed must be held (the user
|
|
5496
|
+
* just signed in via `client.auth.zk.*`).
|
|
5497
|
+
*/
|
|
5498
|
+
async completeAuthorize(opts) {
|
|
5499
|
+
const token = this.deps.session.token;
|
|
5500
|
+
const seed = this.deps.session.seed;
|
|
5501
|
+
if (!token || !seed)
|
|
5502
|
+
throw new Error("Authenticate the user before completing authorization.");
|
|
5503
|
+
const { sealedKeys, fragmentKey } = await sealSeed(seed);
|
|
5504
|
+
const { code } = await this.deps.auth.grant(token, {
|
|
5505
|
+
appId: opts.appId, redirectUri: opts.redirectUri, codeChallenge: opts.codeChallenge, sealedKeys,
|
|
5506
|
+
});
|
|
5507
|
+
const url = new URL(opts.redirectUri);
|
|
5508
|
+
url.searchParams.set("code", code);
|
|
5509
|
+
url.searchParams.set("state", opts.state);
|
|
5510
|
+
url.hash = `k=${fragmentKey}`;
|
|
5511
|
+
return url.toString();
|
|
5512
|
+
}
|
|
5513
|
+
// ---- internals ----------------------------------------------------------
|
|
5514
|
+
location() {
|
|
5515
|
+
const loc = globalThis.location;
|
|
5516
|
+
if (!loc)
|
|
5517
|
+
throw new Error("Hosted auth requires a browser environment.");
|
|
5518
|
+
return loc;
|
|
5519
|
+
}
|
|
5520
|
+
store(v) {
|
|
5521
|
+
const ss = globalThis.sessionStorage;
|
|
5522
|
+
if (!ss)
|
|
5523
|
+
throw new Error("Hosted auth requires sessionStorage.");
|
|
5524
|
+
ss.setItem(PKCE_STORE_KEY, JSON.stringify(v));
|
|
5525
|
+
}
|
|
5526
|
+
take() {
|
|
5527
|
+
const ss = globalThis.sessionStorage;
|
|
5528
|
+
if (!ss)
|
|
5529
|
+
return null;
|
|
5530
|
+
const raw = ss.getItem(PKCE_STORE_KEY);
|
|
5531
|
+
if (!raw)
|
|
5532
|
+
return null;
|
|
5533
|
+
ss.removeItem(PKCE_STORE_KEY);
|
|
5534
|
+
try {
|
|
5535
|
+
return JSON.parse(raw);
|
|
5536
|
+
}
|
|
5537
|
+
catch {
|
|
5538
|
+
return null;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
/** Drop `code`, `state`, and the `#k` fragment from the address bar. */
|
|
5542
|
+
scrubUrl() {
|
|
5543
|
+
const h = globalThis.history;
|
|
5544
|
+
const loc = this.location();
|
|
5545
|
+
if (h?.replaceState)
|
|
5546
|
+
h.replaceState(null, "", loc.pathname);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5239
5550
|
/**
|
|
5240
5551
|
* `client.auth` — user authentication, exposed today through the `zk`
|
|
5241
5552
|
* sub-namespace (`client.auth.zk.login(...)`). Leaving room under `auth` for
|
|
@@ -5484,6 +5795,137 @@ class ZkAuth {
|
|
|
5484
5795
|
this.deps.session.setSeed(seed);
|
|
5485
5796
|
return user;
|
|
5486
5797
|
}
|
|
5798
|
+
/**
|
|
5799
|
+
* Add an **email** as a recovery + login factor (M2 verification-gated:
|
|
5800
|
+
* the server only releases the split-key OPRF eval after the address is
|
|
5801
|
+
* OTP-verified). Sends the code, then returns a `confirm` continuation:
|
|
5802
|
+
*
|
|
5803
|
+
* const { confirm } = await client.auth.zk.enrollEmailFactor("me@x.com");
|
|
5804
|
+
* await confirm(codeFromInbox);
|
|
5805
|
+
*
|
|
5806
|
+
* Requires being signed in with the seed held (like the other enrolls).
|
|
5807
|
+
* Custody note: gated factors trade some custody for recoverability — the
|
|
5808
|
+
* trade-off should be disclosed in the enroll UI.
|
|
5809
|
+
*/
|
|
5810
|
+
async enrollEmailFactor(email) {
|
|
5811
|
+
const seed = this.deps.session.seed;
|
|
5812
|
+
const token = this.deps.session.token;
|
|
5813
|
+
const username = this.deps.session.username;
|
|
5814
|
+
if (!seed || !token || !username) {
|
|
5815
|
+
throw new Error("Sign in first to add a recovery email.");
|
|
5816
|
+
}
|
|
5817
|
+
await this.deps.auth.emailOtpStart({ token, email });
|
|
5818
|
+
return {
|
|
5819
|
+
email,
|
|
5820
|
+
confirm: async (code) => {
|
|
5821
|
+
const { verifyToken, email: verified } = await this.deps.auth.emailOtpVerify({ token, code });
|
|
5822
|
+
const input = await emailFactorInput(username, verified);
|
|
5823
|
+
const { blind, blinded } = gatedBlind(input);
|
|
5824
|
+
const evals = await this.deps.auth.oprfEvaluateGated(username, toBase64(blinded), "email", "enroll", verifyToken);
|
|
5825
|
+
const key = await gatedWrapKey(input, blind, evals.evaluated, evals.evaluated2);
|
|
5826
|
+
const wrapped = await wrapSeed(seed, key);
|
|
5827
|
+
await this.deps.auth.vaultPutFactor(token, { id: "email", type: "email", wrap: wrapped.ct, iv: wrapped.iv, params: { email: verified }, createdAt: Date.now() }, verifyToken);
|
|
5828
|
+
},
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
/**
|
|
5832
|
+
* Recover an account via its enrolled **email** factor. Sends a code to the
|
|
5833
|
+
* enrolled address (the response never reveals whether one exists), then
|
|
5834
|
+
* `confirm(code)` completes: gated read + split-key eval → unwrap seed →
|
|
5835
|
+
* normal ZK login. After this, call {@link changePassword} to set a new
|
|
5836
|
+
* password.
|
|
5837
|
+
*
|
|
5838
|
+
* const { confirm } = await client.auth.zk.recoverWithEmail("alice");
|
|
5839
|
+
* const user = await confirm(codeFromInbox);
|
|
5840
|
+
*/
|
|
5841
|
+
async recoverWithEmail(username, opts = {}) {
|
|
5842
|
+
await this.deps.auth.emailOtpStart({ username });
|
|
5843
|
+
return {
|
|
5844
|
+
confirm: async (code) => {
|
|
5845
|
+
const { verifyToken, email } = await this.deps.auth.emailOtpVerify({ username, code });
|
|
5846
|
+
const { factor } = await this.deps.auth.vaultRead(username, "email", verifyToken);
|
|
5847
|
+
if (!factor?.wrap || !factor?.iv) {
|
|
5848
|
+
throw new Error("This account has no email recovery set up.");
|
|
5849
|
+
}
|
|
5850
|
+
const input = await emailFactorInput(username, email);
|
|
5851
|
+
const { blind, blinded } = gatedBlind(input);
|
|
5852
|
+
const evals = await this.deps.auth.oprfEvaluateGated(username, toBase64(blinded), "email", "recover", verifyToken);
|
|
5853
|
+
const key = await gatedWrapKey(input, blind, evals.evaluated, evals.evaluated2);
|
|
5854
|
+
const seed = await unwrapSeed({ iv: factor.iv, ct: factor.wrap }, key);
|
|
5855
|
+
const identity = await deriveIdentityFromSeed(seed);
|
|
5856
|
+
const user = await this.proveAndStore(username, identity, opts);
|
|
5857
|
+
this.deps.session.setSeed(seed);
|
|
5858
|
+
return user;
|
|
5859
|
+
},
|
|
5860
|
+
};
|
|
5861
|
+
}
|
|
5862
|
+
// --- Google factor (M2.3) ------------------------------------------------
|
|
5863
|
+
// Google is pure authentication UX: its verified ID token gates the
|
|
5864
|
+
// split-key OPRF eval that releases the master seed. The ZK identity layer
|
|
5865
|
+
// is unchanged. The app obtains `idToken` via Google Identity Services and
|
|
5866
|
+
// passes it in; the SDK never renders Google UI.
|
|
5867
|
+
/**
|
|
5868
|
+
* Link a Google account as a recovery + login factor. Requires being signed
|
|
5869
|
+
* in with the seed held. `idToken` is a fresh Google ID token (GIS).
|
|
5870
|
+
*/
|
|
5871
|
+
async enrollGoogleFactor(idToken) {
|
|
5872
|
+
const seed = this.deps.session.seed;
|
|
5873
|
+
const token = this.deps.session.token;
|
|
5874
|
+
const username = this.deps.session.username;
|
|
5875
|
+
if (!seed || !token || !username)
|
|
5876
|
+
throw new Error("Sign in first to link a Google account.");
|
|
5877
|
+
const { verifyToken, sub, emailHint } = await this.deps.auth.googleVerify({ idToken, token });
|
|
5878
|
+
const input = await googleFactorInput(username, sub);
|
|
5879
|
+
const { blind, blinded } = gatedBlind(input);
|
|
5880
|
+
const evals = await this.deps.auth.oprfEvaluateGated(username, toBase64(blinded), "google", "enroll", verifyToken);
|
|
5881
|
+
const key = await gatedWrapKey(input, blind, evals.evaluated, evals.evaluated2);
|
|
5882
|
+
const wrapped = await wrapSeed(seed, key);
|
|
5883
|
+
await this.deps.auth.vaultPutFactor(token, { id: "google", type: "google", wrap: wrapped.ct, iv: wrapped.iv, params: { sub, emailHint: emailHint ?? undefined }, createdAt: Date.now() }, verifyToken);
|
|
5884
|
+
}
|
|
5885
|
+
/**
|
|
5886
|
+
* Sign in with Google — verify the ID token, unlock the seed via the gated
|
|
5887
|
+
* split-key eval, and establish a session. Same machinery serves "Sign in
|
|
5888
|
+
* with Google" and "recover with Google"; the account must have linked the
|
|
5889
|
+
* factor first.
|
|
5890
|
+
*/
|
|
5891
|
+
async loginWithGoogle(username, idToken, opts = {}) {
|
|
5892
|
+
const { verifyToken, sub } = await this.deps.auth.googleVerify({ idToken, username });
|
|
5893
|
+
const { factor } = await this.deps.auth.vaultRead(username, "google", verifyToken);
|
|
5894
|
+
if (!factor?.wrap || !factor?.iv)
|
|
5895
|
+
throw new Error("No Google account is linked to this username.");
|
|
5896
|
+
const input = await googleFactorInput(username, sub);
|
|
5897
|
+
const { blind, blinded } = gatedBlind(input);
|
|
5898
|
+
const evals = await this.deps.auth.oprfEvaluateGated(username, toBase64(blinded), "google", "recover", verifyToken);
|
|
5899
|
+
const key = await gatedWrapKey(input, blind, evals.evaluated, evals.evaluated2);
|
|
5900
|
+
const seed = await unwrapSeed({ iv: factor.iv, ct: factor.wrap }, key);
|
|
5901
|
+
const identity = await deriveIdentityFromSeed(seed);
|
|
5902
|
+
const user = await this.proveAndStore(username, identity, opts);
|
|
5903
|
+
this.deps.session.setSeed(seed);
|
|
5904
|
+
return user;
|
|
5905
|
+
}
|
|
5906
|
+
/**
|
|
5907
|
+
* Register a brand-new, **passwordless** account whose only factor is Google.
|
|
5908
|
+
* A random master seed is generated and wrapped under the Google-gated key;
|
|
5909
|
+
* the user can add a password / passkey / phrase later. The ZK registration
|
|
5910
|
+
* is identical to a password signup.
|
|
5911
|
+
*/
|
|
5912
|
+
async registerWithGoogle(username, idToken) {
|
|
5913
|
+
const seed = randomSeed();
|
|
5914
|
+
const identity = await deriveIdentityFromSeed(seed);
|
|
5915
|
+
const commitment = await this.commitmentFor(identity);
|
|
5916
|
+
await this.deps.auth.register({
|
|
5917
|
+
username,
|
|
5918
|
+
commitment,
|
|
5919
|
+
ecdhPublicKey: await exportPublicKeyBase64(identity.ecdhKeyPair.publicKey),
|
|
5920
|
+
ecdsaPublicKey: await exportPublicKeyBase64(identity.ecdsaKeyPair.publicKey),
|
|
5921
|
+
email: null,
|
|
5922
|
+
});
|
|
5923
|
+
const user = await this.proveAndStore(username, identity, {});
|
|
5924
|
+
this.deps.session.setSeed(seed);
|
|
5925
|
+
// Enroll Google as the sole factor (session + seed are now in hand).
|
|
5926
|
+
await this.enrollGoogleFactor(idToken);
|
|
5927
|
+
return user;
|
|
5928
|
+
}
|
|
5487
5929
|
/**
|
|
5488
5930
|
* Change the password — re-wraps the (unchanged) master seed under the new
|
|
5489
5931
|
* password's OPRF-gated key. The identity/commitment never moves. Requires
|
|
@@ -5561,7 +6003,11 @@ class ZkAuth {
|
|
|
5561
6003
|
this.deps.session.setSeed(seed);
|
|
5562
6004
|
return user;
|
|
5563
6005
|
}
|
|
5564
|
-
/**
|
|
6006
|
+
/**
|
|
6007
|
+
* List the user's enrolled login methods (metadata only). Session-authed.
|
|
6008
|
+
* Gated factors (email/google) carry a `masked` display hint
|
|
6009
|
+
* ("m•••@gmail.com"); the full value is never returned.
|
|
6010
|
+
*/
|
|
5565
6011
|
async listFactors() {
|
|
5566
6012
|
const token = this.deps.session.token;
|
|
5567
6013
|
if (!token)
|
|
@@ -5688,8 +6134,11 @@ class ZkAuth {
|
|
|
5688
6134
|
/** `client.auth` — the auth namespace. Strategies hang off here. */
|
|
5689
6135
|
class AuthNamespace {
|
|
5690
6136
|
zk;
|
|
6137
|
+
/** Centralized hosted auth (auth.muhkoo.dev) — `client.auth.hosted.login(...)`. */
|
|
6138
|
+
hosted;
|
|
5691
6139
|
constructor(deps) {
|
|
5692
6140
|
this.zk = new ZkAuth(deps);
|
|
6141
|
+
this.hosted = new HostedAuth({ auth: deps.auth, session: deps.session, authBaseUrl: deps.authBaseUrl });
|
|
5693
6142
|
}
|
|
5694
6143
|
/** Convenience pass-through: the currently signed-in user, or `null`. */
|
|
5695
6144
|
get user() {
|
|
@@ -7593,6 +8042,7 @@ class AgentsNamespace {
|
|
|
7593
8042
|
* });
|
|
7594
8043
|
* // HTTP-reachable at hello--<slug>.fns.muhkoo.dev
|
|
7595
8044
|
* await client.functions.enable(appId, config.functionId, spaceId); // Space-bound
|
|
8045
|
+
* const out = await client.functions.invoke({ name: 'hello', slug }, { body: { q: 1 } });
|
|
7596
8046
|
*
|
|
7597
8047
|
* These are MANAGEMENT calls — the function itself runs on the accelerator
|
|
7598
8048
|
* (uploaded just-in-time, decrypted only at invocation). Only an app's owner or
|
|
@@ -7606,6 +8056,18 @@ class AgentsNamespace {
|
|
|
7606
8056
|
* triggers: `http` (its own subdomain) and `space` (invoked on Space messages,
|
|
7607
8057
|
* like a Programmable Agent).
|
|
7608
8058
|
*/
|
|
8059
|
+
/** The hosted platform's HTTP-function zone. */
|
|
8060
|
+
const DEFAULT_FN_HOST_SUFFIX = "fns.muhkoo.dev";
|
|
8061
|
+
/** Bodies `fetch` accepts natively — everything else is JSON-encoded. */
|
|
8062
|
+
function isRawBody(body) {
|
|
8063
|
+
return (typeof body === "string" ||
|
|
8064
|
+
body instanceof FormData ||
|
|
8065
|
+
body instanceof Blob ||
|
|
8066
|
+
body instanceof ArrayBuffer ||
|
|
8067
|
+
ArrayBuffer.isView(body) ||
|
|
8068
|
+
body instanceof URLSearchParams ||
|
|
8069
|
+
(typeof ReadableStream !== "undefined" && body instanceof ReadableStream));
|
|
8070
|
+
}
|
|
7609
8071
|
class FunctionsNamespace {
|
|
7610
8072
|
deps;
|
|
7611
8073
|
constructor(deps) {
|
|
@@ -7644,6 +8106,71 @@ class FunctionsNamespace {
|
|
|
7644
8106
|
async disable(appId, functionId, spaceId, opts = {}) {
|
|
7645
8107
|
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
7646
8108
|
}
|
|
8109
|
+
/**
|
|
8110
|
+
* Invoke an HTTP-triggered function and parse its JSON response.
|
|
8111
|
+
*
|
|
8112
|
+
* const out = await client.functions.invoke({ name: 'hello', slug: 'myapp' });
|
|
8113
|
+
* const out = await client.functions.invoke('hello--myapp', { body: { q: 1 } });
|
|
8114
|
+
* const out = await client.functions.invoke('https://hello--myapp.fns.muhkoo.dev/x');
|
|
8115
|
+
*
|
|
8116
|
+
* The request carries the SDK's credentials (`X-Muhkoo-Key` and, when a
|
|
8117
|
+
* user is signed in, `X-Muhkoo-Session`) — the platform passes the session
|
|
8118
|
+
* token through to the function, which can verify it server-side via
|
|
8119
|
+
* `POST /api/auth/verify` to establish the caller's identity.
|
|
8120
|
+
*
|
|
8121
|
+
* Non-2xx responses throw {@link HttpError}. For functions that return
|
|
8122
|
+
* something other than JSON, use {@link invokeRaw}.
|
|
8123
|
+
*/
|
|
8124
|
+
async invoke(target, opts = {}) {
|
|
8125
|
+
const res = await this.invokeRaw(target, opts);
|
|
8126
|
+
let parsed = null;
|
|
8127
|
+
try {
|
|
8128
|
+
parsed = await res.json();
|
|
8129
|
+
}
|
|
8130
|
+
catch {
|
|
8131
|
+
// Non-JSON body — fall through to the status-only error below.
|
|
8132
|
+
}
|
|
8133
|
+
if (!res.ok) {
|
|
8134
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed
|
|
8135
|
+
? String(parsed.error)
|
|
8136
|
+
: `${res.status} ${res.statusText}`;
|
|
8137
|
+
throw new HttpError(msg, res.status, parsed);
|
|
8138
|
+
}
|
|
8139
|
+
return parsed;
|
|
8140
|
+
}
|
|
8141
|
+
/** Invoke an HTTP-triggered function and return the raw `Response`. */
|
|
8142
|
+
async invokeRaw(target, opts = {}) {
|
|
8143
|
+
const init = {
|
|
8144
|
+
method: opts.method ?? (opts.body !== undefined ? "POST" : "GET"),
|
|
8145
|
+
};
|
|
8146
|
+
const headers = new Headers(opts.headers);
|
|
8147
|
+
if (opts.body !== undefined) {
|
|
8148
|
+
if (isRawBody(opts.body)) {
|
|
8149
|
+
init.body = opts.body;
|
|
8150
|
+
}
|
|
8151
|
+
else {
|
|
8152
|
+
init.body = JSON.stringify(opts.body);
|
|
8153
|
+
if (!headers.has("Content-Type"))
|
|
8154
|
+
headers.set("Content-Type", "application/json");
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
init.headers = headers;
|
|
8158
|
+
return this.deps.http.fetch(this.invokeUrl(target, opts.path), init);
|
|
8159
|
+
}
|
|
8160
|
+
/** Resolve an invoke target to the function's absolute URL. */
|
|
8161
|
+
invokeUrl(target, path = "") {
|
|
8162
|
+
const suffix = this.deps.fnHostSuffix ?? DEFAULT_FN_HOST_SUFFIX;
|
|
8163
|
+
let base;
|
|
8164
|
+
if (typeof target === "string") {
|
|
8165
|
+
base = /^https?:\/\//.test(target) ? target : `https://${target}.${suffix}`;
|
|
8166
|
+
}
|
|
8167
|
+
else {
|
|
8168
|
+
base = `https://${target.name}--${target.slug}.${suffix}`;
|
|
8169
|
+
}
|
|
8170
|
+
if (!path)
|
|
8171
|
+
return base;
|
|
8172
|
+
return `${base.replace(/\/+$/, "")}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
8173
|
+
}
|
|
7647
8174
|
path(appId, suffix, opts) {
|
|
7648
8175
|
const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
|
|
7649
8176
|
return `/api/apps/${encodeURIComponent(appId)}/functions${suffix}${q}`;
|
|
@@ -7675,6 +8202,8 @@ class FunctionsNamespace {
|
|
|
7675
8202
|
*/
|
|
7676
8203
|
/** The hosted Muhkoo Accelerator — the default {@link ClientOptions.baseUrl}. */
|
|
7677
8204
|
const DEFAULT_BASE_URL = "https://api.muhkoo.dev";
|
|
8205
|
+
/** The hosted auth SPA — the default {@link ClientOptions.authBaseUrl}. */
|
|
8206
|
+
const DEFAULT_AUTH_BASE_URL = "https://auth.muhkoo.dev";
|
|
7678
8207
|
/**
|
|
7679
8208
|
* Turn the HTTP base URL into a websocket base (`http(s)://` → `ws(s)://`).
|
|
7680
8209
|
* The accelerator serves spaces over both on the same origin.
|
|
@@ -7732,7 +8261,8 @@ class Client {
|
|
|
7732
8261
|
const circuits = options.circuits ?? defaultCircuitUrls(this.baseUrl);
|
|
7733
8262
|
const authClient = new AuthClient({ baseUrl: this.baseUrl, fetch: this.http.fetch });
|
|
7734
8263
|
const wsBaseUrl = toWsBase(this.baseUrl);
|
|
7735
|
-
|
|
8264
|
+
const authBaseUrl = (options.authBaseUrl ?? DEFAULT_AUTH_BASE_URL).replace(/\/+$/, "");
|
|
8265
|
+
this.auth = new AuthNamespace({ auth: authClient, circuits, session: this.session, authBaseUrl });
|
|
7736
8266
|
this.kv = new KvNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
7737
8267
|
this.db = new DbNamespace({ http: this.http });
|
|
7738
8268
|
this.storage = new StorageNamespace({ http: this.http, baseUrl: this.baseUrl, kv: this.kv });
|
|
@@ -7751,7 +8281,7 @@ class Client {
|
|
|
7751
8281
|
cache: spaceKeyCache,
|
|
7752
8282
|
});
|
|
7753
8283
|
this.agents = new AgentsNamespace({ http: this.http });
|
|
7754
|
-
this.functions = new FunctionsNamespace({ http: this.http });
|
|
8284
|
+
this.functions = new FunctionsNamespace({ http: this.http, fnHostSuffix: options.functionsHostSuffix });
|
|
7755
8285
|
}
|
|
7756
8286
|
/** The currently signed-in user, or `null`. */
|
|
7757
8287
|
get user() {
|
|
@@ -8042,5 +8572,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
|
|
|
8042
8572
|
default: loadRsWasm
|
|
8043
8573
|
});
|
|
8044
8574
|
|
|
8045
|
-
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, VaultUnavailableError, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, shardHash, signMessage, toField, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
|
|
8575
|
+
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, VaultUnavailableError, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, toField, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
|
|
8046
8576
|
//# sourceMappingURL=index.js.map
|