@muhkoo/connect 0.10.10-alpha.0 → 0.10.11-alpha.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.
@@ -4537,8 +4537,12 @@ class AuthClient {
4537
4537
  * Read a factor record for `username` (returns an indistinguishable decoy if
4538
4538
  * absent). Gated types (email) require the recover `verifyToken`.
4539
4539
  */
4540
- async vaultRead(username, factorType, verifyToken) {
4541
- const res = await this.fetchFn(`${this.baseUrl}/api/auth/vault`, this.json("POST", { username, factorType, verifyToken }));
4540
+ async vaultRead(username, factorType, verifyToken,
4541
+ /** Passkeys are per-origin: naming the caller's host returns THAT origin's
4542
+ * passkey instead of whichever was enrolled first (which WebAuthn would
4543
+ * refuse). Ignored for other factor types. */
4544
+ rpId) {
4545
+ const res = await this.fetchFn(`${this.baseUrl}/api/auth/vault`, this.json("POST", { username, factorType, verifyToken, rpId }));
4542
4546
  return await this.parse("vaultRead", res);
4543
4547
  }
4544
4548
  /**
@@ -5068,7 +5072,7 @@ function base64UrlDecode(s) {
5068
5072
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
5069
5073
  * `package.json` `version`.
5070
5074
  */
5071
- const VERSION = "0.10.10-alpha.0";
5075
+ const VERSION = "0.10.11-alpha.0";
5072
5076
 
5073
5077
  /**
5074
5078
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -5129,6 +5133,8 @@ class HttpClient {
5129
5133
  /** Origin of {@link baseUrl} — credentials are only attached same-origin. */
5130
5134
  baseOrigin;
5131
5135
  apiKey;
5136
+ /** Scoped machine access token — takes precedence over {@link apiKey} on `X-Muhkoo-Key`. */
5137
+ accessToken;
5132
5138
  getSessionToken;
5133
5139
  onUnauthorized;
5134
5140
  fetchFn;
@@ -5143,6 +5149,7 @@ class HttpClient {
5143
5149
  this.baseOrigin = "";
5144
5150
  }
5145
5151
  this.apiKey = opts.apiKey ?? null;
5152
+ this.accessToken = opts.accessToken ?? null;
5146
5153
  this.getSessionToken = opts.getSessionToken ?? (() => null);
5147
5154
  this.onUnauthorized = opts.onUnauthorized ?? null;
5148
5155
  const f = opts.fetch ?? globalThis.fetch;
@@ -5163,8 +5170,11 @@ class HttpClient {
5163
5170
  // server-reflected path that escapes `baseUrl`) must NOT receive the app
5164
5171
  // key or session token, or it becomes a token-exfiltration vector.
5165
5172
  if (this.isSameOrigin(input)) {
5166
- if (this.apiKey)
5167
- headers.set("X-Muhkoo-Key", this.apiKey);
5173
+ // Access token and app key resolve through the same accelerator path
5174
+ // and ride the same header; the access token takes precedence.
5175
+ const key = this.accessToken ?? this.apiKey;
5176
+ if (key)
5177
+ headers.set("X-Muhkoo-Key", key);
5168
5178
  const token = this.getSessionToken();
5169
5179
  if (token)
5170
5180
  headers.set("X-Muhkoo-Session", token);
@@ -5578,6 +5588,35 @@ async function passkeyPrfCapable() {
5578
5588
  function defaultRpId() {
5579
5589
  return typeof location !== "undefined" ? location.hostname : "";
5580
5590
  }
5591
+ /**
5592
+ * Can a passkey enrolled under `rpId` be used from the CURRENT origin?
5593
+ *
5594
+ * WebAuthn only accepts an RP id that is the origin's own host or a registrable
5595
+ * *parent* of it — so a passkey enrolled on `app.example.com` is unusable on
5596
+ * `app.example.net`, and one enrolled on a full host is unusable from a sibling
5597
+ * host. An app served on BOTH a custom domain and the platform host therefore
5598
+ * needs one passkey per origin.
5599
+ *
5600
+ * Call this BEFORE `navigator.credentials.get()`: passing a foreign rpId throws
5601
+ * a raw "The requested RPID did not match the origin or related origins", which
5602
+ * is a dead end for the user. Checking first lets the caller fall back to normal
5603
+ * sign-in (and offer to enroll a passkey for this origin) instead.
5604
+ */
5605
+ function rpIdUsableForOrigin(rpId, host) {
5606
+ const h = host ?? (typeof location !== "undefined" ? location.hostname : "");
5607
+ if (!rpId || !h)
5608
+ return false;
5609
+ return h === rpId || h.endsWith(`.${rpId}`);
5610
+ }
5611
+ /** Thrown when the account's passkey belongs to a different origin. Callers
5612
+ * should treat this as "no passkey here" and fall back to another factor. */
5613
+ class PasskeyOriginError extends Error {
5614
+ code = "passkey_wrong_origin";
5615
+ constructor(message = "No passkey is enrolled for this site. Sign in another way, then add one for this device.") {
5616
+ super(message);
5617
+ this.name = "PasskeyOriginError";
5618
+ }
5619
+ }
5581
5620
  /**
5582
5621
  * Create a passkey with PRF and return its credential id, a fresh PRF salt, and
5583
5622
  * the PRF output for that salt. Throws if the authenticator can't do PRF.
@@ -6271,12 +6310,22 @@ class ZkAuth {
6271
6310
  * its PRF, unwraps the master seed → identity → session.
6272
6311
  */
6273
6312
  async loginWithPasskey(username, opts = {}) {
6274
- const { factor } = await this.deps.auth.vaultRead(username, "passkey");
6313
+ // Ask for THIS origin's passkey — they're enrolled per-origin, so the
6314
+ // account's first passkey may belong to a different host entirely.
6315
+ const host = defaultRpId();
6316
+ const { factor } = await this.deps.auth.vaultRead(username, "passkey", undefined, host);
6275
6317
  const params = factor?.params;
6276
6318
  if (!factor?.wrap || !factor?.iv || !params?.credentialId || !params?.prfSalt) {
6277
6319
  throw new Error("No passkey is set up for this account.");
6278
6320
  }
6279
- const prfOutput = await evaluatePasskeyPrf(params.rpId ?? defaultRpId(), fromBase64(params.credentialId), fromBase64(params.prfSalt));
6321
+ const rpId = params.rpId ?? host;
6322
+ // Refuse a foreign rpId up front: WebAuthn would throw a raw "requested
6323
+ // RPID did not match the origin", which reads as a hard failure to the
6324
+ // user. A typed error lets the app fall back to another factor and offer
6325
+ // to enroll a passkey for this origin.
6326
+ if (!rpIdUsableForOrigin(rpId, host))
6327
+ throw new PasskeyOriginError();
6328
+ const prfOutput = await evaluatePasskeyPrf(rpId, fromBase64(params.credentialId), fromBase64(params.prfSalt));
6280
6329
  const key = await wrapKeyFromBytes(prfOutput, "muhkoo-passkey-wrap");
6281
6330
  const seed = await unwrapSeed({ iv: factor.iv, ct: factor.wrap }, key);
6282
6331
  const identity = await deriveIdentityFromSeed(seed);
@@ -6296,6 +6345,27 @@ class ZkAuth {
6296
6345
  const { factors } = await this.deps.auth.vaultFactors(token);
6297
6346
  return factors;
6298
6347
  }
6348
+ /**
6349
+ * Is a passkey enrolled that can unlock from THIS origin?
6350
+ *
6351
+ * Passkeys are origin-bound, so an app served on both a custom domain and the
6352
+ * platform host needs one per host. Use this to decide whether to offer
6353
+ * "Unlock with passkey" or go straight to sign-in — prompting with another
6354
+ * origin's passkey fails with a raw WebAuthn error. Requires a session
6355
+ * (metadata read); returns false when signed out or unsupported.
6356
+ */
6357
+ async hasPasskeyForThisOrigin() {
6358
+ if (!passkeySupported())
6359
+ return false;
6360
+ try {
6361
+ const host = defaultRpId();
6362
+ const factors = await this.listFactors();
6363
+ return factors.some((f) => f.type === "passkey" && rpIdUsableForOrigin(f.rpId, host));
6364
+ }
6365
+ catch {
6366
+ return false;
6367
+ }
6368
+ }
6299
6369
  /** Remove a login method by id (can't remove your only one). Session-authed. */
6300
6370
  async removeFactor(id) {
6301
6371
  const token = this.deps.session.token;
@@ -10109,6 +10179,59 @@ class FunctionsNamespace {
10109
10179
  }
10110
10180
  }
10111
10181
 
10182
+ /**
10183
+ * `client.accessTokens` — manage an app's **access tokens**: scoped, expiring,
10184
+ * non-ZK machine-to-machine credentials (`mk_<env>_at_…`).
10185
+ *
10186
+ * const { plaintext } = await client.accessTokens.create(appId, {
10187
+ * scopes: ["db:read", "db:write"], env: "live", expiresInDays: 30,
10188
+ * label: "ci-runner",
10189
+ * });
10190
+ * // hand `plaintext` to the machine caller as its ClientOptions.accessToken
10191
+ *
10192
+ * These are MANAGEMENT calls — creating/listing/revoking tokens is authorized by
10193
+ * the developer session (same as the `agents`/`functions` namespaces). The token
10194
+ * itself resolves through the SAME accelerator path as an app key (the
10195
+ * `X-Muhkoo-Key` header), but is scoped to a fixed vocabulary of permissions and
10196
+ * expires. The plaintext is returned ONCE, on create — store it then; it can't
10197
+ * be read back.
10198
+ */
10199
+ /** All valid {@link Scope}s, in canonical order. */
10200
+ const ACCESS_TOKEN_SCOPES = [
10201
+ "db:read",
10202
+ "db:write",
10203
+ "kv:read",
10204
+ "kv:write",
10205
+ "storage:read",
10206
+ "storage:write",
10207
+ "messages:read",
10208
+ "messages:write",
10209
+ "functions:invoke",
10210
+ "ai:infer",
10211
+ ];
10212
+ class AccessTokensNamespace {
10213
+ deps;
10214
+ constructor(deps) {
10215
+ this.deps = deps;
10216
+ }
10217
+ /** List the app's access tokens (metadata only — never the plaintext). */
10218
+ async list(appId) {
10219
+ const res = await this.deps.http.get(this.path(appId, ""));
10220
+ return res.tokens ?? [];
10221
+ }
10222
+ /** Mint an access token (owner/editor only). The plaintext is returned once. */
10223
+ async create(appId, input) {
10224
+ return this.deps.http.post(this.path(appId, ""), input);
10225
+ }
10226
+ /** Revoke an access token by its `keyId`. */
10227
+ async revoke(appId, keyId) {
10228
+ return this.deps.http.del(this.path(appId, `/${encodeURIComponent(keyId)}`));
10229
+ }
10230
+ path(appId, suffix) {
10231
+ return `/api/apps/${encodeURIComponent(appId)}/access-tokens${suffix}`;
10232
+ }
10233
+ }
10234
+
10112
10235
  /**
10113
10236
  * `ChatKeyVault` — SDK-owned lifecycle for a member's long-lived ratchet/space
10114
10237
  * keypair (the connect `KeyStore` entry keyed by the member id / username).
@@ -11356,6 +11479,8 @@ class Client {
11356
11479
  agents;
11357
11480
  /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
11358
11481
  functions;
11482
+ /** Access tokens — `client.accessTokens.create(appId, …)`, etc. */
11483
+ accessTokens;
11359
11484
  /** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
11360
11485
  offline;
11361
11486
  session;
@@ -11409,6 +11534,7 @@ class Client {
11409
11534
  this.http = new HttpClient({
11410
11535
  baseUrl: this.baseUrl,
11411
11536
  apiKey: options.apiKey,
11537
+ accessToken: options.accessToken,
11412
11538
  getSessionToken: () => this.session.token,
11413
11539
  fetch: reportingFetch,
11414
11540
  // Self-heal stale sessions: on a 401, try a silent re-auth (only
@@ -11485,6 +11611,7 @@ class Client {
11485
11611
  });
11486
11612
  this.agents = new AgentsNamespace({ http: this.http });
11487
11613
  this.functions = new FunctionsNamespace({ http: this.http, fnHostSuffix: options.functionsHostSuffix });
11614
+ this.accessTokens = new AccessTokensNamespace({ http: this.http });
11488
11615
  // Replay deferred shard uploads on reconnect. The bytes live in the
11489
11616
  // Cache API keyed by hash; re-PUT them through a (defer-free) shard
11490
11617
  // client so a failure surfaces as transient and the entry stays queued.
@@ -11634,6 +11761,7 @@ function MuhkooDB(meta) {
11634
11761
  table: meta.table ?? String(propertyKey),
11635
11762
  access: meta.access ?? "read",
11636
11763
  description: meta.description,
11764
+ backend: meta.backend ?? false,
11637
11765
  });
11638
11766
  });
11639
11767
  }
@@ -11673,9 +11801,12 @@ function ejectAgentPrompt(appClass) {
11673
11801
  for (const s of d.spaces)
11674
11802
  lines.push(`- ${s.name}: ${s.description}`);
11675
11803
  }
11676
- if (d.tables.length) {
11804
+ // Backend-only tables are deliberately withheld — the agent should never be
11805
+ // told about server-managed / internal state it can't (and shouldn't) touch.
11806
+ const promptTables = d.tables.filter((t) => !t.backend);
11807
+ if (promptTables.length) {
11677
11808
  lines.push("", "App database tables:");
11678
- for (const t of d.tables) {
11809
+ for (const t of promptTables) {
11679
11810
  const verbs = t.access === "write"
11680
11811
  ? "read & write — db_query/db_get/db_insert/db_update/db_delete"
11681
11812
  : "read-only — db_query/db_get";
@@ -11693,7 +11824,7 @@ function ejectAgentPrompt(appClass) {
11693
11824
  // How to respond — the load-bearing behavioral contract. Without this, models
11694
11825
  // (especially gpt-oss) tend to run tool calls and then end their turn with no
11695
11826
  // user-facing message, so the user sees the work happen but gets no reply.
11696
- const hasTools = d.tables.length > 0 || d.functions.length > 0;
11827
+ const hasTools = promptTables.length > 0 || d.functions.length > 0;
11697
11828
  lines.push("", "How to respond:");
11698
11829
  if (hasTools) {
11699
11830
  lines.push("- When you need information or need to make a change, call your tools immediately — don't ask the user to do it, and don't describe or name the tools to them.", "- After your tool calls return, you MUST send one final reply to the channel, in plain language, that answers the request using the results.");
@@ -11903,5 +12034,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
11903
12034
  default: loadRsWasm
11904
12035
  });
11905
12036
 
11906
- export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, buildCommitment, compareHlc, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isP2pCapable, isProvisional, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
12037
+ export { ACCESS_TOKEN_SCOPES, AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AccessTokensNamespace, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PasskeyOriginError, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, buildCommitment, compareHlc, compilePrograms, createPasskeyWithPrf, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultRpId, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, evaluatePasskeyPrf, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isP2pCapable, isProvisional, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, passkeyPrfCapable, passkeySupported, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, rpIdUsableForOrigin, sealSeed, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
11907
12038
  //# sourceMappingURL=index.js.map