@mida-context/sdk 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 +21 -0
- package/README.md +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7115 -0
- package/dist/types/api/app.d.ts +99 -0
- package/dist/types/api/auth-pure.d.ts +51 -0
- package/dist/types/api/auth.d.ts +17 -0
- package/dist/types/api/authorize.d.ts +24 -0
- package/dist/types/api/batch-deny.d.ts +22 -0
- package/dist/types/api/batch-routes.d.ts +69 -0
- package/dist/types/api/batch-store.d.ts +96 -0
- package/dist/types/api/batcher.d.ts +220 -0
- package/dist/types/api/browser.d.ts +14 -0
- package/dist/types/api/chain-budget.d.ts +61 -0
- package/dist/types/api/chain-views.d.ts +87 -0
- package/dist/types/api/client.d.ts +295 -0
- package/dist/types/api/deny-overlay.d.ts +81 -0
- package/dist/types/api/errors.d.ts +30 -0
- package/dist/types/api/file-stores.d.ts +7 -0
- package/dist/types/api/index.d.ts +17 -0
- package/dist/types/api/secure-fs.d.ts +12 -0
- package/dist/types/api/store.d.ts +51 -0
- package/dist/types/api/stores.d.ts +171 -0
- package/dist/types/api/verify-assertion.d.ts +22 -0
- package/dist/types/api/wire.d.ts +27 -0
- package/dist/types/chain/abis.d.ts +2125 -0
- package/dist/types/chain/browser.d.ts +20 -0
- package/dist/types/chain/deployment-fs.d.ts +15 -0
- package/dist/types/chain/deployment.d.ts +33 -0
- package/dist/types/chain/deployments.generated.d.ts +1 -0
- package/dist/types/chain/gas.d.ts +47 -0
- package/dist/types/chain/history.d.ts +52 -0
- package/dist/types/chain/index.d.ts +13 -0
- package/dist/types/chain/local.d.ts +34 -0
- package/dist/types/chain/logs.d.ts +62 -0
- package/dist/types/chain/placements.d.ts +92 -0
- package/dist/types/chain/read-scope.d.ts +66 -0
- package/dist/types/chain/registry.d.ts +16 -0
- package/dist/types/chain/sponsored.d.ts +93 -0
- package/dist/types/chain/transport.d.ts +53 -0
- package/dist/types/chain/writes.d.ts +165 -0
- package/dist/types/crypto/aead.d.ts +5 -0
- package/dist/types/crypto/bytes.d.ts +4 -0
- package/dist/types/crypto/derive.d.ts +20 -0
- package/dist/types/crypto/index.d.ts +6 -0
- package/dist/types/crypto/object.d.ts +42 -0
- package/dist/types/crypto/payload.d.ts +21 -0
- package/dist/types/crypto/wraps.d.ts +36 -0
- package/dist/types/grant-advisor/advise.d.ts +23 -0
- package/dist/types/grant-advisor/authority.d.ts +44 -0
- package/dist/types/grant-advisor/index.d.ts +5 -0
- package/dist/types/grant-advisor/manifest.d.ts +66 -0
- package/dist/types/grant-advisor/policy.d.ts +91 -0
- package/dist/types/grant-advisor/signatures.d.ts +10 -0
- package/dist/types/mida-context-sdk/daemon.d.ts +27 -0
- package/dist/types/mida-context-sdk/errors.d.ts +27 -0
- package/dist/types/mida-context-sdk/index.d.ts +5 -0
- package/dist/types/mida-context-sdk/local.d.ts +40 -0
- package/dist/types/mida-context-sdk/mida.d.ts +65 -0
- package/dist/types/mida-context-sdk/transport.d.ts +137 -0
- package/dist/types/protocol/batch.d.ts +161 -0
- package/dist/types/protocol/constants.d.ts +56 -0
- package/dist/types/protocol/errors.d.ts +15 -0
- package/dist/types/protocol/ids.d.ts +72 -0
- package/dist/types/protocol/index.d.ts +11 -0
- package/dist/types/protocol/namespaces.d.ts +15 -0
- package/dist/types/protocol/owner-link.d.ts +146 -0
- package/dist/types/protocol/typed-data.d.ts +365 -0
- package/dist/types/protocol/types.d.ts +215 -0
- package/dist/types/protocol/webauthn-assertion.d.ts +29 -0
- package/dist/types/protocol/wire.d.ts +8 -0
- package/dist/types/sdk/agent.d.ts +279 -0
- package/dist/types/sdk/batched.d.ts +68 -0
- package/dist/types/sdk/connect.d.ts +88 -0
- package/dist/types/sdk/index.d.ts +7 -0
- package/dist/types/sdk/request-store.d.ts +26 -0
- package/dist/types/storage/index.d.ts +23 -0
- package/package.json +50 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Address } from "../protocol/index.js";
|
|
2
|
+
import type { Deployment } from "../chain/index.js";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import type { BatchingOptions } from "./batch-routes.js";
|
|
5
|
+
import { BudgetedReader } from "./chain-budget.js";
|
|
6
|
+
import type { ContextRecordView, RegistryReader } from "./chain-views.js";
|
|
7
|
+
import { DenyOverlay } from "./deny-overlay.js";
|
|
8
|
+
import type { StoredObject } from "./store.js";
|
|
9
|
+
import type { ContextStores, StoreLimits } from "./stores.js";
|
|
10
|
+
export declare const CANCELLATION_MAX_LIFETIME_SECONDS = 300n;
|
|
11
|
+
/**
|
|
12
|
+
* How long one read-scope token — and the server-side memo it opens — may live (in-12 N-10): a
|
|
13
|
+
* handoff's 7.5 s deadline is the operation it exists for, so 8 s covers it with no room for a
|
|
14
|
+
* stale answer to outlive the operation. Fixed from first sight, never extended: a renewed token
|
|
15
|
+
* points at the same bucket, whose expiry was set when the bucket was born.
|
|
16
|
+
*/
|
|
17
|
+
export declare const READ_SCOPE_TTL_MS = 8000;
|
|
18
|
+
/** Manifest GET responses are allowed this stale before the envelope is re-verified against Monad. */
|
|
19
|
+
export declare const MANIFEST_VERIFY_CACHE_SECONDS = 60n;
|
|
20
|
+
/**
|
|
21
|
+
* How far ahead of the wall clock a staged manifest's issuedAt may run. The field is written on chain
|
|
22
|
+
* time (provisionAgent stamps the latest block timestamp) and the two clocks legitimately skew — local
|
|
23
|
+
* rigs move chain time forward on purpose — so the wall clock must not hard-gate staging. The bound
|
|
24
|
+
* still rejects absurd values cheaply; the authoritative check is verifySignedManifest's, against chain
|
|
25
|
+
* time, once the agent registers — and an unverified envelope is swept at 24 h regardless.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MANIFEST_STAGING_FUTURE_SECONDS = 86400n;
|
|
28
|
+
/**
|
|
29
|
+
* The pending-bytes quota re-checks unmarked uploads in batches of 8, at most this many chain reads
|
|
30
|
+
* per PUT — and never more than the request's remaining budget after authorization
|
|
31
|
+
* (MAX_CHAIN_READS_PER_REQUEST bounds every route's total Monad reads).
|
|
32
|
+
*/
|
|
33
|
+
export declare const PENDING_CHECK_BATCH = 8;
|
|
34
|
+
export declare const PENDING_CHECK_MAX_READS = 16;
|
|
35
|
+
/**
|
|
36
|
+
* An unmarked upload only counts against the pending-bytes quota for this long. Stale orphans —
|
|
37
|
+
* uploads whose anchor transaction never landed — would otherwise sit in the quota sum forever and
|
|
38
|
+
* block every new PUT. Age is safe HERE because the quota only bounds storage cost; it is never
|
|
39
|
+
* safe in the read path, where an unmarked row must be checked against Monad however old it is —
|
|
40
|
+
* the anchored_at mark is only written on a verified read, so an old row can still hold a real
|
|
41
|
+
* record.
|
|
42
|
+
*/
|
|
43
|
+
export declare const PENDING_QUOTA_WINDOW_SECONDS = 86400;
|
|
44
|
+
/**
|
|
45
|
+
* Per-IP request limiting, injected by the deployment. The hosted Worker's [[ratelimits]] bindings adapt
|
|
46
|
+
* to it; a self-hosted Node server may pass its own (or none — the README says a reverse proxy is needed
|
|
47
|
+
* then). `signed` tells the limiter which bucket the request counts against.
|
|
48
|
+
*/
|
|
49
|
+
export interface RequestLimiter {
|
|
50
|
+
check(input: {
|
|
51
|
+
ip: string;
|
|
52
|
+
signed: boolean;
|
|
53
|
+
}): Promise<boolean>;
|
|
54
|
+
}
|
|
55
|
+
export interface ContextApiOptions {
|
|
56
|
+
reader: RegistryReader;
|
|
57
|
+
deployment: Deployment;
|
|
58
|
+
/** The file-backed stores' directory; used only when `stores` is not given. */
|
|
59
|
+
dataDir?: string;
|
|
60
|
+
/** Injected persistence — the hosted worker passes its D1 stores here. */
|
|
61
|
+
stores?: ContextStores;
|
|
62
|
+
/** Upload-abuse limits; any field overrides the shared defaults in DEFAULT_STORE_LIMITS. */
|
|
63
|
+
limits?: Partial<StoreLimits>;
|
|
64
|
+
/** Per-IP request-rate limiting; default none (a self-hoster limits at their reverse proxy). */
|
|
65
|
+
limiter?: RequestLimiter;
|
|
66
|
+
/** Wall-clock seconds for request freshness. Chain time decides capability expiry. */
|
|
67
|
+
clock?: () => bigint;
|
|
68
|
+
/**
|
|
69
|
+
* The batched-save lane (BatchAnchor). Absent: no /batch/* surface exists and every pre-batch
|
|
70
|
+
* behaviour is unchanged. Present: the routes mount, gated inside by `batching.enabled`.
|
|
71
|
+
*/
|
|
72
|
+
batching?: BatchingOptions;
|
|
73
|
+
/**
|
|
74
|
+
* The name of THIS store's RPC-endpoint setting, used in CHAIN_MISCONFIGURED/RPC_AUTH_REJECTED
|
|
75
|
+
* hints (in-14 F-4): "RPC_URL" on the hosted worker, "network.json rpcUrl" on a midad laptop
|
|
76
|
+
* store. Default is a setting-neutral "RPC endpoint".
|
|
77
|
+
*/
|
|
78
|
+
rpcHint?: string;
|
|
79
|
+
}
|
|
80
|
+
type Env = {
|
|
81
|
+
Variables: {
|
|
82
|
+
signer: Address;
|
|
83
|
+
body: Uint8Array;
|
|
84
|
+
chain: BudgetedReader;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/** §12.3: an object is served only once Monad holds matching commitments for it. */
|
|
88
|
+
export declare function isAnchored(stored: StoredObject, record: ContextRecordView | null): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Minimal Context API (§12). A thin storage and authorization service, never a decryptor: it holds ciphertext,
|
|
91
|
+
* immutable manifests and reader wraps, and every agent operation passes the §12.1 ordered checks against Monad.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createContextApi(options: ContextApiOptions): {
|
|
94
|
+
app: Hono<Env, import("hono/types").BlankSchema, "/">;
|
|
95
|
+
overlay: DenyOverlay;
|
|
96
|
+
store: import("./stores.js").ObjectStore;
|
|
97
|
+
limits: StoreLimits;
|
|
98
|
+
};
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Address } from "../protocol/index.js";
|
|
2
|
+
import type { NonceStore } from "./stores.js";
|
|
3
|
+
/**
|
|
4
|
+
* The request-authentication pieces that touch no Node API — everything in `auth.ts` minus the
|
|
5
|
+
* file-backed ReplayGuard. They live in their own module so the owner-page bundle can reach them
|
|
6
|
+
* through `client.js` without pulling `node:fs` into a browser build: `client.js` imports from
|
|
7
|
+
* here, `auth.js` keeps ReplayGuard and re-exports this module so every existing importer of
|
|
8
|
+
* `auth.js` sees the same names it always did.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AUTH_HEADERS: {
|
|
11
|
+
readonly signer: "x-mida-signer";
|
|
12
|
+
readonly timestamp: "x-mida-timestamp";
|
|
13
|
+
readonly nonce: "x-mida-nonce";
|
|
14
|
+
readonly signature: "x-mida-signature";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* in-9 R-5, hardened in in-12 N-10: an optional token naming the ONE logical read operation a
|
|
18
|
+
* request belongs to (a handoff, a whats-new refresh). The server mints it — a random id, an
|
|
19
|
+
* expiry and an HMAC over the requester's own signer — and hands it out on every authenticated
|
|
20
|
+
* response, so the client only ever replays what it was issued. Requests carrying the same token
|
|
21
|
+
* share the answers their identical NON-AUTHORIZATION chain reads already paid for; a request
|
|
22
|
+
* without it, or with a forged or expired one, is simply unscoped — the token names a cache
|
|
23
|
+
* bucket, never an authorization, and authorization answers are never memoized across requests.
|
|
24
|
+
*/
|
|
25
|
+
export declare const READ_SCOPE_HEADER = "x-mida-read-scope";
|
|
26
|
+
/** The token shape the server honors: `0x` + 64-hex random id, base-36 expiry ms, 64-hex HMAC. */
|
|
27
|
+
export declare const READ_SCOPE_TOKEN_PATTERN: RegExp;
|
|
28
|
+
export declare const REQUEST_WINDOW_SECONDS = 60n;
|
|
29
|
+
/** Builds the §12.1 canonical target from a URL: path plus query sorted by key. Repeated keys are rejected. */
|
|
30
|
+
export declare function targetOf(url: URL): string;
|
|
31
|
+
/**
|
|
32
|
+
* The presence and format checks on the four authentication headers — everything that can be decided without
|
|
33
|
+
* touching state. Runs before body parsing and long before signature verification: a request whose headers
|
|
34
|
+
* are missing or malformed never costs a nonce record, a store read or a chain read.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertAuthHeaderShape(headers: Headers): void;
|
|
37
|
+
/**
|
|
38
|
+
* §12.1 authentication: an EIP-712 MidaHttpRequestV1 signature over method, canonical target, raw body bytes,
|
|
39
|
+
* timestamp and nonce, under the "Mida Context API" domain for this chain and registry. Returns the proven signer.
|
|
40
|
+
* Authorization happens afterwards and separately.
|
|
41
|
+
*/
|
|
42
|
+
export declare function authenticateRequest(input: {
|
|
43
|
+
method: string;
|
|
44
|
+
url: URL;
|
|
45
|
+
headers: Headers;
|
|
46
|
+
body: Uint8Array;
|
|
47
|
+
chainId: bigint;
|
|
48
|
+
capabilityRegistry: Address;
|
|
49
|
+
now: bigint;
|
|
50
|
+
replay: NonceStore;
|
|
51
|
+
}): Promise<Address>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Address, Hex } from "../protocol/index.js";
|
|
2
|
+
import type { NonceStore } from "./stores.js";
|
|
3
|
+
export { AUTH_HEADERS, READ_SCOPE_HEADER, READ_SCOPE_TOKEN_PATTERN, REQUEST_WINDOW_SECONDS, targetOf, assertAuthHeaderShape, authenticateRequest } from "./auth-pure.js";
|
|
4
|
+
/**
|
|
5
|
+
* The file-backed NonceStore: a durable (signer, nonce) record for the §12.1 validity window. Each accepted pair is
|
|
6
|
+
* written to disk, atomically and synchronously, before authentication returns, so neither a restart nor a crash can
|
|
7
|
+
* forget a nonce that was accepted. Expired entries are deleted by `sweep`, which the scheduled job calls — never the
|
|
8
|
+
* request path. An unreadable store fails closed rather than starting empty.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ReplayGuard implements NonceStore {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(file: string);
|
|
13
|
+
/** Atomically checks and records a nonce; an entry already present means this exact request was seen before. */
|
|
14
|
+
consume(signer: Address, nonce: Hex, signedAt: bigint, now: bigint): Promise<void>;
|
|
15
|
+
/** Removes nonces whose signed timestamp is more than 60 seconds old; returns how many were deleted. */
|
|
16
|
+
sweep(now: bigint): Promise<number>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Address, AgentRecord, Hex } from "../protocol/index.js";
|
|
2
|
+
import type { CapabilityView, RegistryReader } from "./chain-views.js";
|
|
3
|
+
import type { DenyOverlay } from "./deny-overlay.js";
|
|
4
|
+
export interface AgentAuthorization {
|
|
5
|
+
agentId: Hex;
|
|
6
|
+
agent: AgentRecord;
|
|
7
|
+
capability: CapabilityView;
|
|
8
|
+
now: bigint;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* §12.1 normative, fail-closed validation order for every agent operation. Step 1 (authentication) has already
|
|
12
|
+
* produced `signer`. Each later step stops at the first failure with its §12.6 code. A final exact `hasAuthority`
|
|
13
|
+
* read keeps the API chain-bounded: it can deny sooner than Monad, never allow what Monad does not.
|
|
14
|
+
*/
|
|
15
|
+
export declare function authorizeAgent(input: {
|
|
16
|
+
reader: RegistryReader;
|
|
17
|
+
overlay: DenyOverlay;
|
|
18
|
+
signer: Address;
|
|
19
|
+
owner: Address;
|
|
20
|
+
capabilityId: Hex | undefined;
|
|
21
|
+
namespaceId: Hex;
|
|
22
|
+
permission: number;
|
|
23
|
+
agentKeyVersion?: number;
|
|
24
|
+
}): Promise<AgentAuthorization>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BatchSaveRow } from "./batch-store.js";
|
|
2
|
+
import type { RegistryReader } from "./chain-views.js";
|
|
3
|
+
import type { DenyOverlay } from "./deny-overlay.js";
|
|
4
|
+
/** One row's answer at send time: submit it, keep it HELD, or mark it dead. */
|
|
5
|
+
export type BatchGateVerdict = "send" | "hold" | "reject";
|
|
6
|
+
export interface BatchRowGate {
|
|
7
|
+
/**
|
|
8
|
+
* One row's verdict. The batcher memoizes calls within a pass keyed on the fields a verdict
|
|
9
|
+
* may read — owner, signer, namespaceId, save.message.parentId, save.message.rootAuthor — so
|
|
10
|
+
* an implementation must not answer from anything else (in-11 R-9).
|
|
11
|
+
*/
|
|
12
|
+
check(row: BatchSaveRow): Promise<BatchGateVerdict>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The store's own deny overlay plus live Monad authority, sharing the DenyOverlay instance the
|
|
16
|
+
* HTTP routes use so a deny staged or cancelled through the API is exactly what the next tick
|
|
17
|
+
* sees. Injected per-row because the knowledge lives beside the queue, not inside the signer.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createBatchDenyGate(input: {
|
|
20
|
+
reader: RegistryReader;
|
|
21
|
+
overlay: DenyOverlay;
|
|
22
|
+
}): BatchRowGate;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Hono, MiddlewareHandler } from "hono";
|
|
2
|
+
import type { LocalAccount } from "viem";
|
|
3
|
+
import type { Address } from "../protocol/index.js";
|
|
4
|
+
import type { Deployment } from "../chain/index.js";
|
|
5
|
+
import type { BudgetedReader } from "./chain-budget.js";
|
|
6
|
+
import type { DenyOverlay } from "./deny-overlay.js";
|
|
7
|
+
import type { StoreLimits } from "./stores.js";
|
|
8
|
+
import type { BatchStore } from "./batch-store.js";
|
|
9
|
+
/** What app.ts mounts under when batching is configured; absent means no batch surface at all. */
|
|
10
|
+
export interface BatchingOptions {
|
|
11
|
+
/** Kill switch — routes stay mounted but POSTs answer 503 BATCHING_DISABLED. */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The BatchAnchor contract this lane anchors through. Absent only on a boot-degraded lane
|
|
15
|
+
* (`configInvalid` set) whose anchor never parsed — /batch/status then omits the field rather
|
|
16
|
+
* than echo a value a client could wrongly match on.
|
|
17
|
+
*/
|
|
18
|
+
batchAnchor?: Address;
|
|
19
|
+
store: BatchStore;
|
|
20
|
+
/**
|
|
21
|
+
* Signs admission receipts; a store key, unrelated to any chain identity. Only the enabled lane
|
|
22
|
+
* ever signs — with `enabled: false` the field may be absent entirely (the worker keeps the
|
|
23
|
+
* secret optional then so a missing one cannot take the whole store down).
|
|
24
|
+
*/
|
|
25
|
+
receiptAccount?: LocalAccount;
|
|
26
|
+
/**
|
|
27
|
+
* Proves batchAnchor is the contract this deployment expects — run lazily on the first batch
|
|
28
|
+
* request and cached by the implementer. A throw refuses the batch surface with its message.
|
|
29
|
+
*/
|
|
30
|
+
verifyAnchor?: () => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Trial gate: when present and non-empty, POST /batch/saves admits only saves whose signed
|
|
33
|
+
* owner is on this list (compared case-insensitively — entries are matched against the
|
|
34
|
+
* wire's lowercase address). Absent or empty leaves admission exactly as it was.
|
|
35
|
+
*/
|
|
36
|
+
ownerAllowlist?: readonly Address[];
|
|
37
|
+
/**
|
|
38
|
+
* in-20 T-1: set when the batching configuration failed to build — a bad BATCHER_PRIVATE_KEY,
|
|
39
|
+
* a missing coordinator binding, a malformed allowlist. The store itself keeps serving; the
|
|
40
|
+
* lane reports `enabled: false, reason: "config-invalid"` and every batch write answers 503
|
|
41
|
+
* BATCH_UNAVAILABLE carrying this message, which names the variable and its rule — never its
|
|
42
|
+
* value. Reads keep answering: queue rows already stored still list.
|
|
43
|
+
*/
|
|
44
|
+
configInvalid?: string;
|
|
45
|
+
/** Wake-up for the batcher, called exactly once per accepted save. */
|
|
46
|
+
notify: () => void;
|
|
47
|
+
/** Runs one submission round; invoked by POST /batch/flush after its checks pass. */
|
|
48
|
+
flush: () => Promise<void>;
|
|
49
|
+
/** Wall clock in ms — injectable so tests control receivedAt and the 10 s flush window. */
|
|
50
|
+
now?: () => number;
|
|
51
|
+
}
|
|
52
|
+
/** The request variables the app's middleware sets — kept structurally identical to app.ts's Env. */
|
|
53
|
+
export interface BatchRouteEnv {
|
|
54
|
+
Variables: {
|
|
55
|
+
signer: Address;
|
|
56
|
+
body: Uint8Array;
|
|
57
|
+
chain: BudgetedReader;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export interface BatchRouteDeps {
|
|
61
|
+
batching: BatchingOptions;
|
|
62
|
+
deployment: Deployment;
|
|
63
|
+
limits: StoreLimits;
|
|
64
|
+
overlay: DenyOverlay;
|
|
65
|
+
authenticated: (maxBodyBytes: number) => MiddlewareHandler<BatchRouteEnv>;
|
|
66
|
+
/** The app's strict JSON body parser — INVALID_WIRE on any malformed input. */
|
|
67
|
+
json: <T>(body: Uint8Array) => T;
|
|
68
|
+
}
|
|
69
|
+
export declare function mountBatchRoutes(app: Hono<BatchRouteEnv>, deps: BatchRouteDeps): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Address, Hex } from "../protocol/index.js";
|
|
2
|
+
import type { BatchedReadItem, BatchedSaveWire } from "./client.js";
|
|
3
|
+
/**
|
|
4
|
+
* QUEUED → SUBMITTED → ANCHORED | REJECTED, plus HELD — the in-3 state for a save whose author is
|
|
5
|
+
* on the store's active deny list. A held row is off the send path (`takeQueued` never returns it)
|
|
6
|
+
* but not dead: every batcher tick re-checks it — deny still active → stays HELD; the revoke
|
|
7
|
+
* landed → REJECTED; the deny cleared and the grant still valid → back to QUEUED and sent.
|
|
8
|
+
*/
|
|
9
|
+
export type BatchSaveState = "QUEUED" | "SUBMITTED" | "ANCHORED" | "REJECTED" | "HELD";
|
|
10
|
+
export interface BatchSaveRow {
|
|
11
|
+
contextId: Hex;
|
|
12
|
+
owner: Address;
|
|
13
|
+
namespaceId: Hex;
|
|
14
|
+
/** Request signer that submitted the save — may GET the row without a READ grant. */
|
|
15
|
+
signer: Address;
|
|
16
|
+
/** The full signed save (message, signature, manifest, ciphertext hex). */
|
|
17
|
+
save: BatchedSaveWire;
|
|
18
|
+
state: BatchSaveState;
|
|
19
|
+
/** Rejection reason name for REJECTED rows (BATCH_REJECT keys from the contract). */
|
|
20
|
+
reason: string | null;
|
|
21
|
+
batchId: Hex | null;
|
|
22
|
+
position: number | null;
|
|
23
|
+
lineageId: Hex | null;
|
|
24
|
+
version: number | null;
|
|
25
|
+
proof: Hex[] | null;
|
|
26
|
+
/** ms since epoch when the save was accepted into the queue. */
|
|
27
|
+
receivedAt: number;
|
|
28
|
+
/** ms since epoch when the anchor confirmed, null until then. */
|
|
29
|
+
anchoredAt: number | null;
|
|
30
|
+
}
|
|
31
|
+
export interface BatchStore {
|
|
32
|
+
/** Returns "exists" when a row with this contextId is already stored — any state. */
|
|
33
|
+
insert(row: BatchSaveRow): Promise<"inserted" | "exists">;
|
|
34
|
+
get(contextId: Hex): Promise<BatchSaveRow | null>;
|
|
35
|
+
/** QUEUED + SUBMITTED + ANCHORED + HELD for one owner/namespace, oldest first. REJECTED never lists. */
|
|
36
|
+
listForReader(owner: Address, namespaceId: Hex): Promise<BatchSaveRow[]>;
|
|
37
|
+
/** Atomically moves up to `limit` QUEUED rows to SUBMITTED under `batchId`, oldest first. */
|
|
38
|
+
takeQueued(limit: number, batchId: Hex): Promise<BatchSaveRow[]>;
|
|
39
|
+
markAnchored(contextId: Hex, fields: {
|
|
40
|
+
batchId: Hex;
|
|
41
|
+
position: number;
|
|
42
|
+
lineageId: Hex;
|
|
43
|
+
version: number;
|
|
44
|
+
proof: Hex[];
|
|
45
|
+
anchoredAt: number;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
markRejected(contextId: Hex, reason: string): Promise<void>;
|
|
48
|
+
/** SUBMITTED rows of `batchId` go back to QUEUED (a failed submission retried whole). */
|
|
49
|
+
requeue(batchId: Hex): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* One SUBMITTED row goes back to QUEUED, its batch tag cleared — a row the take could not
|
|
52
|
+
* check or could not hold retries on a later take instead of dying with its batch (in-11 R-2/R-9).
|
|
53
|
+
*/
|
|
54
|
+
requeueRow(contextId: Hex): Promise<void>;
|
|
55
|
+
/** Every HELD row, oldest first — the batcher's per-tick deny re-check set. */
|
|
56
|
+
listHeld(): Promise<BatchSaveRow[]>;
|
|
57
|
+
/** A QUEUED or SUBMITTED row goes HELD; the batch tag clears so a requeue-by-batch never revives it. */
|
|
58
|
+
hold(contextId: Hex): Promise<void>;
|
|
59
|
+
/** A HELD row returns to QUEUED — the deny cleared and Monad still authorizes the save. */
|
|
60
|
+
releaseHeld(contextId: Hex): Promise<void>;
|
|
61
|
+
/** Monotonic receipt sequence — counts every accepted save, never reused. */
|
|
62
|
+
nextSequence(): Promise<bigint>;
|
|
63
|
+
countQueued(): Promise<number>;
|
|
64
|
+
/** ms since epoch of the signer's last accepted flush, or null. */
|
|
65
|
+
lastFlush(signer: Address): Promise<number | null>;
|
|
66
|
+
setLastFlush(signer: Address, atMs: number): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/** The wire-facing read shape of a row — anchor fields only on ANCHORED rows. */
|
|
69
|
+
export declare function batchReadItem(row: BatchSaveRow): BatchedReadItem;
|
|
70
|
+
export declare class FsBatchStore implements BatchStore {
|
|
71
|
+
#private;
|
|
72
|
+
/** Rows live at `dataDir/batch/<contextId>.json`; sequence and flush marks in `dataDir/batch/meta.json`. */
|
|
73
|
+
constructor(dataDir: string);
|
|
74
|
+
insert(row: BatchSaveRow): Promise<"inserted" | "exists">;
|
|
75
|
+
get(contextId: Hex): Promise<BatchSaveRow | null>;
|
|
76
|
+
listForReader(owner: Address, namespaceId: Hex): Promise<BatchSaveRow[]>;
|
|
77
|
+
takeQueued(limit: number, batchId: Hex): Promise<BatchSaveRow[]>;
|
|
78
|
+
markAnchored(contextId: Hex, fields: {
|
|
79
|
+
batchId: Hex;
|
|
80
|
+
position: number;
|
|
81
|
+
lineageId: Hex;
|
|
82
|
+
version: number;
|
|
83
|
+
proof: Hex[];
|
|
84
|
+
anchoredAt: number;
|
|
85
|
+
}): Promise<void>;
|
|
86
|
+
markRejected(contextId: Hex, reason: string): Promise<void>;
|
|
87
|
+
requeue(batchId: Hex): Promise<number>;
|
|
88
|
+
requeueRow(contextId: Hex): Promise<void>;
|
|
89
|
+
listHeld(): Promise<BatchSaveRow[]>;
|
|
90
|
+
hold(contextId: Hex): Promise<void>;
|
|
91
|
+
releaseHeld(contextId: Hex): Promise<void>;
|
|
92
|
+
nextSequence(): Promise<bigint>;
|
|
93
|
+
countQueued(): Promise<number>;
|
|
94
|
+
lastFlush(signer: Address): Promise<number | null>;
|
|
95
|
+
setLastFlush(signer: Address, atMs: number): Promise<void>;
|
|
96
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { LocalAccount } from "viem";
|
|
2
|
+
import type { Address, Hex } from "../protocol/index.js";
|
|
3
|
+
import type { Deployment } from "../chain/index.js";
|
|
4
|
+
import type { BatchedSaveWire } from "./client.js";
|
|
5
|
+
import type { BatchRowGate } from "./batch-deny.js";
|
|
6
|
+
import type { BatchStore } from "./batch-store.js";
|
|
7
|
+
/** The batcher's view of time: whoever hosts it supplies the real clock source (setTimeout, a DO alarm). */
|
|
8
|
+
export interface BatcherTimer {
|
|
9
|
+
set(atMs: number): Promise<void> | void;
|
|
10
|
+
clear(): Promise<void> | void;
|
|
11
|
+
pending(): Promise<boolean> | boolean;
|
|
12
|
+
}
|
|
13
|
+
/** One SaveAnchored log the chain recorded for a batch — `position` is its slot in the accepted array. */
|
|
14
|
+
export interface AnchoredLog {
|
|
15
|
+
contextId: Hex;
|
|
16
|
+
agentId: Hex;
|
|
17
|
+
position: number;
|
|
18
|
+
lineageId: Hex;
|
|
19
|
+
version: number;
|
|
20
|
+
leafHash: Hex;
|
|
21
|
+
}
|
|
22
|
+
/** One SaveRejected log — `index` is the save's position in the submitted array, not the leaf array. */
|
|
23
|
+
export interface RejectedLog {
|
|
24
|
+
index: number;
|
|
25
|
+
reason: number;
|
|
26
|
+
}
|
|
27
|
+
/** Everything the batcher needs from Monad, and nothing more — fakes implement this in tests. */
|
|
28
|
+
export interface BatcherChain {
|
|
29
|
+
/**
|
|
30
|
+
* Sends submitBatch; { exists: true } means the contract already holds this batchId
|
|
31
|
+
* (BatchExists). A real send reports the receipt's gasUsed — the measurement the batcher
|
|
32
|
+
* re-sizes the next take from. { exists: true } carries no receipt, so it carries no number.
|
|
33
|
+
*/
|
|
34
|
+
submit(batchId: Hex, saves: BatchedSaveWire[]): Promise<{
|
|
35
|
+
transactionHash: Hex;
|
|
36
|
+
gasUsed: bigint;
|
|
37
|
+
} | {
|
|
38
|
+
exists: true;
|
|
39
|
+
}>;
|
|
40
|
+
anchoredLogs(batchId: Hex): Promise<AnchoredLog[]>;
|
|
41
|
+
rejectedLogs(batchId: Hex): Promise<RejectedLog[]>;
|
|
42
|
+
/** The BatchAnchored event for this batchId — who submitted it and how it split; null if none. */
|
|
43
|
+
batchAnchored(batchId: Hex): Promise<{
|
|
44
|
+
submitter: Address;
|
|
45
|
+
acceptedCount: number;
|
|
46
|
+
rejectedCount: number;
|
|
47
|
+
} | null>;
|
|
48
|
+
batchOf(batchId: Hex): Promise<{
|
|
49
|
+
root: Hex;
|
|
50
|
+
blockNumber: bigint;
|
|
51
|
+
acceptedCount: number;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* The batchIds that anchored these contextIds, in one bounded historical scan for the whole
|
|
55
|
+
* set — used for ALREADY_ANCHORED/STALE_PARENT heals. `oldestReceivedAtMs` is the oldest
|
|
56
|
+
* receivedAt among the rows being healed: a save cannot anchor before it reached the queue, so
|
|
57
|
+
* the scan only needs the window that age implies. Map keys and values are lowercase; a
|
|
58
|
+
* contextId never anchored is simply absent.
|
|
59
|
+
*/
|
|
60
|
+
findAnchorings(contextIds: Hex[], oldestReceivedAtMs: number): Promise<Map<string, Hex>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Attempt bookkeeping for an in-flight batch — the clock the unproven-retry cadence and the
|
|
64
|
+
* batch.unproven-stale alert run on. Persisted beside the batch's ordered contextIds so a restart
|
|
65
|
+
* does not reset the hour the operator's alert counts.
|
|
66
|
+
*/
|
|
67
|
+
export interface BatchAttempt {
|
|
68
|
+
/** ms since epoch when the batch's first submit went out. */
|
|
69
|
+
firstAt: number;
|
|
70
|
+
/** ms since epoch of the most recent submit or re-resolve attempt. */
|
|
71
|
+
lastAt: number;
|
|
72
|
+
/** How many submit/resolve attempts the batch has seen. */
|
|
73
|
+
count: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The crash-recovery journal: which batchIds are in flight and, for each, the ordered contextIds of
|
|
77
|
+
* the submitted array (the only map from SaveRejected.index back to a row). `record` is called with
|
|
78
|
+
* an empty list BEFORE the rows are even taken, then again with the real list BEFORE submit — a
|
|
79
|
+
* process that dies in either gap still leaves a discoverable batchId for `recover()` to requeue.
|
|
80
|
+
*/
|
|
81
|
+
export interface BatchJournal {
|
|
82
|
+
/** Every recorded batchId not yet cleared. */
|
|
83
|
+
list(): Promise<Hex[]>;
|
|
84
|
+
record(batchId: Hex, contextIds: Hex[]): Promise<void>;
|
|
85
|
+
contextIds(batchId: Hex): Promise<Hex[] | null>;
|
|
86
|
+
/** The attempt record for a journaled batch — null for one written before tracking existed. */
|
|
87
|
+
attempt(batchId: Hex): Promise<BatchAttempt | null>;
|
|
88
|
+
/** Stamps a submit/re-resolve attempt: creates the record, else moves lastAt and bumps count. */
|
|
89
|
+
noteAttempt(batchId: Hex, atMs: number): Promise<void>;
|
|
90
|
+
clear(batchId: Hex): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/** Journal for tests and embedded use — process memory only, so it cannot recover a real restart. */
|
|
93
|
+
export declare class MemoryBatchJournal implements BatchJournal {
|
|
94
|
+
readonly batches: Map<string, `0x${string}`[]>;
|
|
95
|
+
readonly attempts: Map<string, BatchAttempt>;
|
|
96
|
+
list(): Promise<Hex[]>;
|
|
97
|
+
record(batchId: Hex, contextIds: Hex[]): Promise<void>;
|
|
98
|
+
contextIds(batchId: Hex): Promise<Hex[] | null>;
|
|
99
|
+
attempt(batchId: Hex): Promise<BatchAttempt | null>;
|
|
100
|
+
noteAttempt(batchId: Hex, atMs: number): Promise<void>;
|
|
101
|
+
clear(batchId: Hex): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Journal for the Node side: one JSON file beside the batch store's directory (never inside it —
|
|
105
|
+
* FsBatchStore reads every *.json under dataDir/batch/ as a row). Mutations serialize behind the same
|
|
106
|
+
* in-process promise chain the store uses.
|
|
107
|
+
*/
|
|
108
|
+
export declare class FsBatchJournal implements BatchJournal {
|
|
109
|
+
#private;
|
|
110
|
+
constructor(path: string);
|
|
111
|
+
list(): Promise<Hex[]>;
|
|
112
|
+
record(batchId: Hex, contextIds: Hex[]): Promise<void>;
|
|
113
|
+
contextIds(batchId: Hex): Promise<Hex[] | null>;
|
|
114
|
+
attempt(batchId: Hex): Promise<BatchAttempt | null>;
|
|
115
|
+
noteAttempt(batchId: Hex, atMs: number): Promise<void>;
|
|
116
|
+
clear(batchId: Hex): Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The Node timer: `set` replaces the pending wakeup (the batcher itself only ever asks for one at a
|
|
120
|
+
* time, or for the later gap-end instant); `unref` keeps a pending batch from holding the process open.
|
|
121
|
+
*/
|
|
122
|
+
export declare function createNodeTimer(fire: () => void): BatcherTimer;
|
|
123
|
+
/** Thrown when the logs a chain adapter returns cannot be the batch the contract recorded. */
|
|
124
|
+
export declare class BatchRootMismatchError extends Error {
|
|
125
|
+
readonly code: "ROOT_MISMATCH";
|
|
126
|
+
constructor(batchId: Hex, expected: Hex, rebuilt: Hex);
|
|
127
|
+
}
|
|
128
|
+
export interface BatcherOptions {
|
|
129
|
+
store: BatchStore;
|
|
130
|
+
chain: BatcherChain;
|
|
131
|
+
timer: BatcherTimer;
|
|
132
|
+
now: () => number;
|
|
133
|
+
/**
|
|
134
|
+
* The hard upper bound on saves per batch — never exceeded no matter what the gas math says.
|
|
135
|
+
* The take a run actually uses is usually smaller: the number of saves that fit the gas budget
|
|
136
|
+
* at the per-save cost learned so far.
|
|
137
|
+
*/
|
|
138
|
+
cap: number;
|
|
139
|
+
/**
|
|
140
|
+
* The gas budget each take is sized against; default is the "batch.submit" ceiling. Takes are
|
|
141
|
+
* planned at 95% of it so estimate drift between sizing and sending does not turn a legal take
|
|
142
|
+
* into a refusal.
|
|
143
|
+
*/
|
|
144
|
+
gasBudget?: bigint;
|
|
145
|
+
/**
|
|
146
|
+
* The per-save gas the first take assumes; default 66,264, the Sep 24 testnet sweep's measured
|
|
147
|
+
* 60-save figure (docs/evidence/batch-anchor-sweep-2026-09-24.json). The first real receipt
|
|
148
|
+
* replaces it.
|
|
149
|
+
*/
|
|
150
|
+
initialGasPerSave?: bigint;
|
|
151
|
+
waitMs: number;
|
|
152
|
+
/**
|
|
153
|
+
* The batch submitter — one ingredient of the batchId and the identity resolve() proves a landed
|
|
154
|
+
* batch belongs to: submitBatch has no caller check, so an id is never trusted on its own.
|
|
155
|
+
*/
|
|
156
|
+
submitter: Address;
|
|
157
|
+
/**
|
|
158
|
+
* The 32 random bytes inside each batchId — keccak(submitter, sequence, salt). The salt is what
|
|
159
|
+
* makes the next id unpredictable: a derivable id can be pre-claimed by a foreign submitBatch,
|
|
160
|
+
* which would hand that batcher's rows a stranger's outcomes. The journal records the id before
|
|
161
|
+
* the submit goes out, so recover() never needs to recompute it. Injectable for tests that must
|
|
162
|
+
* predict (or replay) an id; default is crypto.getRandomValues — present in Workers and Node.
|
|
163
|
+
*/
|
|
164
|
+
salt?: () => Hex;
|
|
165
|
+
log?: (record: Record<string, unknown>) => void;
|
|
166
|
+
/** Minimum milliseconds between two real submissions; default 1000. */
|
|
167
|
+
minGapMs?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Persistent record of in-flight batches — the only way `recover()` can find a batch a dead
|
|
170
|
+
* process submitted. Without one, recovery sees only this process's own submissions.
|
|
171
|
+
*/
|
|
172
|
+
journal?: BatchJournal;
|
|
173
|
+
/**
|
|
174
|
+
* The per-row authority check run at send time and on every held-row re-check (in-3 I5):
|
|
175
|
+
* a row whose author sits on the store's active deny list is HELD instead of submitted — the
|
|
176
|
+
* deny overlay is the only thing that knows a revoke is pending on Monad, and the contract
|
|
177
|
+
* cannot see it. Held rows are re-checked each run: still denied → keep holding; no deny and no
|
|
178
|
+
* live authority → REJECTED with the reason the contract would have given; deny cleared and
|
|
179
|
+
* authority live → back to QUEUED and sent. A check that cannot complete parks the row back
|
|
180
|
+
* in QUEUED (in-11 R-9) — only an affirmative verdict may hold it. Absent, the batcher
|
|
181
|
+
* submits exactly as before.
|
|
182
|
+
*/
|
|
183
|
+
gate?: BatchRowGate;
|
|
184
|
+
}
|
|
185
|
+
export declare class Batcher {
|
|
186
|
+
#private;
|
|
187
|
+
constructor(options: BatcherOptions);
|
|
188
|
+
/**
|
|
189
|
+
* One new save arrived. At the learned cap the batch goes now; below it the first save arms the
|
|
190
|
+
* wait window and every later save in the window leaves the armed timer exactly where it is.
|
|
191
|
+
*/
|
|
192
|
+
notify(): Promise<void>;
|
|
193
|
+
/** Flush pokes the queue awake: drop the pending wait and run — the gap rule still applies. */
|
|
194
|
+
flush(): Promise<void>;
|
|
195
|
+
run(): Promise<{
|
|
196
|
+
batchId: Hex;
|
|
197
|
+
accepted: number;
|
|
198
|
+
rejected: number;
|
|
199
|
+
} | null>;
|
|
200
|
+
/** Re-resolve a batch against the chain — used by run() and by recover() for journaled batches. */
|
|
201
|
+
resolve(batchId: Hex): Promise<void>;
|
|
202
|
+
/**
|
|
203
|
+
* Startup recovery: every batch still recorded in the journal (plus any this process itself has
|
|
204
|
+
* in flight) is checked against the chain — landed batches resolve their rows, batches the chain
|
|
205
|
+
* never saw hand their rows back to the queue. Afterwards a non-empty queue arms the timer again.
|
|
206
|
+
*/
|
|
207
|
+
recover(): Promise<void>;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The real chain adapter: viem clients over the deployment's RPC, submitBatch through sendContract.
|
|
211
|
+
* The send runs under the "batch.submit" ceiling (28M gas) — sized under Monad's 30M
|
|
212
|
+
* per-transaction limit rather than the 6M "revoke.agent" ceiling a batch used to borrow — and
|
|
213
|
+
* reports the receipt's gasUsed so the batcher can size the next take from the real per-save cost.
|
|
214
|
+
*/
|
|
215
|
+
export declare function createBatcherChain(input: {
|
|
216
|
+
rpcUrl: string;
|
|
217
|
+
deployment: Deployment;
|
|
218
|
+
account: LocalAccount;
|
|
219
|
+
now?: () => number;
|
|
220
|
+
}): BatcherChain;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-safe surface of @mida/api for the owner page bundle: the hosted-store client and
|
|
3
|
+
* the chain view readers the flows need. app.ts (Hono server), auth.ts's ReplayGuard and
|
|
4
|
+
* secure-fs/stores stay out — the owner page is a client, not the server. client.js imports
|
|
5
|
+
* AUTH_HEADERS and targetOf from auth-pure.js, the node-free half of auth.ts, so nothing in this
|
|
6
|
+
* graph reaches node:fs (the bundle test scans the output for "node:").
|
|
7
|
+
* verify-assertion.js is exported as types only — its value code uses Buffer.
|
|
8
|
+
*/
|
|
9
|
+
export { ContextApiClient, LIST_PARTIAL_MAX_RETRIES, StoreHttpError } from "./client.js";
|
|
10
|
+
export type { ContextApiClientOptions, ContextApiRoutes, ListObjectsResult } from "./client.js";
|
|
11
|
+
export { RegistryReader } from "./chain-views.js";
|
|
12
|
+
export type { CapabilityView, ContextRecordView } from "./chain-views.js";
|
|
13
|
+
export type { AnchoredObject, ObjectUploadBody } from "./wire.js";
|
|
14
|
+
export type { WebAuthnAssertionInput } from "./verify-assertion.js";
|