@terminal3/t3n-sdk 2.9.0 → 2.11.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/README.md CHANGED
@@ -764,6 +764,72 @@ The algorithm matches `node/wasm/src/otp.rs` byte-for-byte: `String((hash * 31 +
764
764
  - **Spec alignment.** The walkthrough follows the as-built code, which differs from earlier T3-TS-026 versions on two points: `user-upsert` has no `op:` discriminator (dispatch is implicit on input shape), and the function for L2 session creation is `tee:user/contracts::create-kyc-provider-session` rather than `tee:kyc/contracts::create-session`. See [T3-TS-026 v0.6.0 changelog](../../docs/specs/T3-TS-026-kyc-frontend-integration.md) and [MAT-1374](https://linear.app/terminal3/issue/MAT-1374) for the deferred discriminator-vs-split decision.
765
765
  - **`STRICT_SCENARIO=true`** in the environment turns scenario assertion failures into a non-zero exit code (otherwise they're logged but the demo exits zero). `--scenario` always exits non-zero on a terminal-status mismatch.
766
766
 
767
+ ### Tenant Developer Demo
768
+
769
+ Three-entity walkthrough: an **Admin** admits a tenant, the **Tenant** registers the Duffel flight contract and seeds credentials, a **User** (created via `demo:dev`) authenticates and searches / books flights. Each step is a separate command so you can run them independently.
770
+
771
+ #### Three entities
772
+
773
+ | Entity | Key env var | Role |
774
+ |---|---|---|
775
+ | Admin | `ADMIN_KEY` | Signs `tenant.admit` via the admin API |
776
+ | Tenant | `TENANT_KEY` | Registers contract, creates KV map, injects Duffel API key |
777
+ | User | `USER_KEY` | Authenticates and calls `search-offers` / `book-offer` on behalf of an AI agent |
778
+
779
+ #### Environment variables
780
+
781
+ | Variable | Default | Used by |
782
+ |---|---|---|
783
+ | `CRYPTO_NODE_URL` | `http://localhost:3000` | all commands |
784
+ | `ADMIN_KEY` | `0x000...0001` | `admit` |
785
+ | `TENANT_KEY` | `0x000...0002` | `admit`, `setup` |
786
+ | `DUFFEL_API_KEY` | `duffel_test_placeholder` | `setup` |
787
+ | `FLIGHT_WASM_PATH` | _(unset)_ | `setup` — path to compiled `z-tenant-flight` WASM; falls back to a placeholder |
788
+ | `USER_KEY` | `0x000...0003` | `search`, `book` — ETH key of a user already registered via `demo:dev` |
789
+ | `OFFER_ID` | _(required)_ | `book` — offer ID printed by `search` |
790
+ | `TOTAL_AMOUNT` | _(required)_ | `book` — total price printed by `search` |
791
+ | `TOTAL_CURRENCY` | _(required)_ | `book` — currency code printed by `search` |
792
+
793
+ #### Step 1 — Admit the tenant (Admin)
794
+
795
+ ```bash
796
+ ADMIN_KEY=0x... TENANT_KEY=0x... pnpm demo:tenant:admit
797
+ ```
798
+
799
+ #### Step 2 — Register contract + seed credentials (Tenant)
800
+
801
+ ```bash
802
+ TENANT_KEY=0x... DUFFEL_API_KEY=duffel_test_... FLIGHT_WASM_PATH=../z-tenant-flight/target/wasm32-wasip2/release/z_tenant_flight.wasm pnpm demo:tenant:setup
803
+ ```
804
+
805
+ This registers `z:<tid>:duffel-flight`, creates the `z:<tid>:secrets` KV map restricted to that contract, then calls `store-credentials` on the contract to write the Duffel API key into the map.
806
+
807
+ #### Step 3 — Create the user (User)
808
+
809
+ Use the standard user demo — no changes needed:
810
+
811
+ ```bash
812
+ USER_KEY=0x... pnpm demo:dev
813
+ ```
814
+
815
+ #### Step 4 — Search for flights (User / Agent)
816
+
817
+ ```bash
818
+ USER_KEY=0x... pnpm demo:tenant:search
819
+ ```
820
+
821
+ Prints up to 5 offers. The last line of output is the exact `book` command with the correct `OFFER_ID`, `TOTAL_AMOUNT`, and `TOTAL_CURRENCY` pre-filled.
822
+
823
+ #### Step 5 — Book a flight (User / Agent)
824
+
825
+ ```bash
826
+ USER_KEY=0x... OFFER_ID=off_... TOTAL_AMOUNT=199.00 TOTAL_CURRENCY=GBP pnpm demo:tenant:book
827
+ ```
828
+
829
+ The passenger record is a hardcoded fixture (Jane Doe, GB passport). PII enters the TEE enclave and is never returned — only `booking_id`, `pnr`, and `status` cross the WIT boundary back to the caller.
830
+
831
+ ---
832
+
767
833
  ## WASM Integration
768
834
 
769
835
  The SDK can work with both real T3n WASM components and mock components for testing.
package/dist/index.d.ts CHANGED
@@ -301,12 +301,19 @@ interface OidcCredentials {
301
301
  interface BaseAuthInput {
302
302
  method: AuthMethod;
303
303
  }
304
+ /**
305
+ * Ethereum authentication options
306
+ */
307
+ interface EthAuthOptions {
308
+ ethDerived?: boolean;
309
+ }
304
310
  /**
305
311
  * Ethereum authentication input
306
312
  */
307
313
  interface EthAuthInput extends BaseAuthInput {
308
314
  method: AuthMethod.Ethereum;
309
315
  address: string;
316
+ ethDerived?: boolean;
310
317
  }
311
318
  /**
312
319
  * OIDC authentication input
@@ -322,7 +329,7 @@ type AuthInput = EthAuthInput | OidcAuthInput;
322
329
  /**
323
330
  * Helper functions to create auth inputs
324
331
  */
325
- declare function createEthAuthInput(address: string): EthAuthInput;
332
+ declare function createEthAuthInput(address: string, options?: EthAuthOptions): EthAuthInput;
326
333
  declare function createOidcAuthInput(credentials: OidcCredentials): OidcAuthInput;
327
334
 
328
335
  /**
@@ -802,6 +809,109 @@ interface OrgDataActionWire {
802
809
  signature: string;
803
810
  }
804
811
 
812
+ /**
813
+ * Token-metering types — T3-TS-030 wire shapes.
814
+ *
815
+ * Mirrors `node/primitives/src/token.rs`. `u128` fields land on the
816
+ * wire as JSON numbers (same convention as the existing
817
+ * `token.get-balance` response) — JS clients with histories that
818
+ * exceed 2⁵³ tokens should switch to a streaming JSON parser; the
819
+ * common case fits in `number` losslessly.
820
+ */
821
+ /**
822
+ * `primitives::token::BalanceRow` — caller's credit row.
823
+ *
824
+ * `available` and `reserved` are `u128` on the server; the wire
825
+ * format is a JSON number. Callers should not assume bigint until
826
+ * the SDK migrates to a streaming parser (tracked separately).
827
+ */
828
+ interface BalanceRow {
829
+ available: number;
830
+ reserved: number;
831
+ last_settled_seq_no: number;
832
+ version: number;
833
+ credit_exhausted: boolean;
834
+ }
835
+ /**
836
+ * `primitives::token::TokenTxKind` snake_case wire alphabet. Every
837
+ * value listed here is accepted by the server-side
838
+ * `token.get-usage` `kinds` filter.
839
+ */
840
+ type TokenTxKind = "mint" | "burn" | "charge" | "transfer" | "bridge_mint_attest" | "bridge_burn_attest";
841
+ /**
842
+ * Per-caller view direction. `"in"` = caller's balance went up;
843
+ * `"out"` = caller's balance went down. Derived host-side from
844
+ * `(caller_did, tx.src, tx.dst)`; the same `seq_no` appears in both
845
+ * parties' usage feeds with opposite directions on a transfer.
846
+ */
847
+ type Direction = "in" | "out";
848
+ /**
849
+ * Discriminated union mirroring `primitives::token::ChargeReason`.
850
+ * Present only when the row's `kind === "charge"`.
851
+ */
852
+ type ChargeReason = {
853
+ kind: "contract_register";
854
+ script_name: string;
855
+ version: string;
856
+ } | {
857
+ kind: "invocation";
858
+ script_name: string;
859
+ function: string;
860
+ fuel_consumed: number;
861
+ fuel_tokens: number;
862
+ host_call_count: number;
863
+ host_call_tokens: number;
864
+ } | {
865
+ kind: "kv_bytes";
866
+ map: string;
867
+ } | {
868
+ kind: "cas_bytes";
869
+ backend: string;
870
+ } | {
871
+ kind: "outbox_egress";
872
+ upstream: string;
873
+ };
874
+ /**
875
+ * One row in the caller's usage feed — a per-caller projection of
876
+ * the underlying `TokenTx`. `counterparty` is the other party from
877
+ * the caller's perspective: `tx.dst` on outbound entries, `tx.src`
878
+ * on inbound. `null` when the underlying tx has no counterparty
879
+ * (mints have no `src`; burns and charges have no `dst`).
880
+ */
881
+ interface UsageEntry {
882
+ seq_no: number;
883
+ kind: TokenTxKind;
884
+ amount: number;
885
+ timestamp_ms: number;
886
+ direction: Direction;
887
+ counterparty?: string | null;
888
+ reason?: ChargeReason | null;
889
+ note?: string | null;
890
+ }
891
+ /**
892
+ * Response shape of `token.get-usage` — caller's balance plus a
893
+ * paginated slice of their most-recent `token:tx_log` entries.
894
+ *
895
+ * `next_cursor` is the inclusive upper-bound `seq_no` to pass back
896
+ * as `after_seq` to fetch the next page. `null` (or absent) means
897
+ * the caller's history is fully drained.
898
+ */
899
+ interface UsagePage {
900
+ balance: BalanceRow;
901
+ entries: UsageEntry[];
902
+ next_cursor?: number | null;
903
+ }
904
+ /**
905
+ * Request shape — all fields optional. `limit` clamps to
906
+ * `1..=200` server-side; out-of-range values snap silently to the
907
+ * bounds. `kinds: []` is treated as "no filter".
908
+ */
909
+ interface GetUsageOptions {
910
+ limit?: number;
911
+ after_seq?: number;
912
+ kinds?: TokenTxKind[];
913
+ }
914
+
805
915
  /**
806
916
  * Public types export for T3n SDK
807
917
  */
@@ -882,6 +992,12 @@ interface Transport {
882
992
  * @returns Promise that resolves to the JSON-RPC response
883
993
  */
884
994
  send(request: JsonRpcRequest, headers: Record<string, string>): Promise<JsonRpcResponse>;
995
+ /**
996
+ * Send a JSON-RPC request with an attached binary blob (multipart/form-data).
997
+ * Part 1 (name=jsonrpc): JSON-RPC envelope.
998
+ * Part 2 (name=blob): raw binary bytes (e.g. WASM bytecode).
999
+ */
1000
+ sendMultipart?(request: JsonRpcRequest, headers: Record<string, string>, blob: Blob): Promise<JsonRpcResponse>;
885
1001
  /**
886
1002
  * Optional accessor for the latest Set-Cookie header value.
887
1003
  * (Useful in Node.js demos/tests; browsers block HttpOnly cookies.)
@@ -904,6 +1020,7 @@ declare class HttpTransport implements Transport {
904
1020
  getLastSetCookie(): string | null;
905
1021
  getLastResponseHeaders(): Record<string, string>;
906
1022
  send(request: JsonRpcRequest, headers: Record<string, string>): Promise<JsonRpcResponse>;
1023
+ sendMultipart(request: JsonRpcRequest, headers: Record<string, string>, blob: Blob): Promise<JsonRpcResponse>;
907
1024
  }
908
1025
  /**
909
1026
  * Mock transport for testing
@@ -924,6 +1041,7 @@ declare class MockTransport implements Transport {
924
1041
  private responseHeaders;
925
1042
  private lastResponseHeaders;
926
1043
  private requests;
1044
+ private multipartRequests;
927
1045
  /**
928
1046
  * Mock a response for a specific method
929
1047
  */
@@ -954,10 +1072,16 @@ declare class MockTransport implements Transport {
954
1072
  request: JsonRpcRequest;
955
1073
  headers: Record<string, string>;
956
1074
  }>;
1075
+ getMultipartRequests(): Array<{
1076
+ request: JsonRpcRequest;
1077
+ headers: Record<string, string>;
1078
+ blob: Blob;
1079
+ }>;
957
1080
  /**
958
1081
  * Clear all recorded requests
959
1082
  */
960
1083
  clearRequests(): void;
1084
+ sendMultipart(request: JsonRpcRequest, headers: Record<string, string>, blob: Blob): Promise<JsonRpcResponse>;
961
1085
  send(request: JsonRpcRequest, headers: Record<string, string>): Promise<JsonRpcResponse>;
962
1086
  }
963
1087
 
@@ -1287,6 +1411,26 @@ declare class T3nClient {
1287
1411
  * optionally validates it with a schema.
1288
1412
  */
1289
1413
  execute(payload: unknown): Promise<string>;
1414
+ /**
1415
+ * Fetch the caller's usage feed — balance plus a bounded slice of
1416
+ * recent token-ledger entries (charges, mints, transfers), newest
1417
+ * first. T3-TS-030 Phase 1D step A (`token.get-usage`).
1418
+ *
1419
+ * `limit` defaults to 50 server-side and clamps silently to
1420
+ * `1..=200`. `afterSeq` is the inclusive upper-bound `seq_no`
1421
+ * passed back from a previous page's `next_cursor` to walk older
1422
+ * entries. `kinds` filters by `TokenTxKind`; an empty array is
1423
+ * treated as "no filter".
1424
+ *
1425
+ * Unlike {@link execute}, the body of this RPC is plaintext —
1426
+ * `token.get-usage` is a session-authed read endpoint and the
1427
+ * server-side handler does not run the encryption layer.
1428
+ */
1429
+ getUsage(opts?: GetUsageOptions): Promise<UsagePage>;
1430
+ /**
1431
+ * Execute an action with an attached binary blob using multipart RPC.
1432
+ */
1433
+ executeWithBlob(payload: unknown, blob: Blob): Promise<string>;
1290
1434
  /**
1291
1435
  * Execute an action and JSON-decode the response.
1292
1436
  *
@@ -1627,6 +1771,42 @@ declare class T3nClient {
1627
1771
  * Send an RPC request with automatic encryption/decryption
1628
1772
  */
1629
1773
  private sendRpcRequest;
1774
+ /**
1775
+ * Send a session-authenticated JSON-RPC request with plaintext
1776
+ * params/result (no SessionEncryption layer). Used for tenant-facing
1777
+ * read endpoints (`token.get-balance`, `token.get-usage`) where the
1778
+ * server-side handler reads the JSON envelope directly.
1779
+ *
1780
+ * Auto-attaches the `Session-Id` header; surfaces JSON-RPC errors
1781
+ * the same way as {@link sendRpcRequest} (typed
1782
+ * `InsufficientCreditError` when applicable, `RpcError` otherwise).
1783
+ * Returns the parsed `result` field — typically a JSON object the
1784
+ * caller will narrow to the endpoint's response type.
1785
+ */
1786
+ private sendUnencryptedSessionRpc;
1787
+ /**
1788
+ * Inspect a JSON-RPC response for an `error` field and throw the
1789
+ * appropriate typed exception. No-op when `response.error` is
1790
+ * absent. Shared by every RPC path so wire-shape changes in the
1791
+ * server's error envelope (typed-error subclasses, request-id
1792
+ * placement, detail formatting) only need to land in one place.
1793
+ *
1794
+ * JSON-RPC `error.message` is the generic category string
1795
+ * ("Invalid params", "Internal error", …). The node attaches the
1796
+ * actionable text and per-request correlation id in `error.data`
1797
+ * — see `node/api/src/responses/rpc.rs::from_service_error`.
1798
+ * Extract both so callers (and toasts that only render `.message`)
1799
+ * get the real reason plus an id an operator can grep in
1800
+ * `api::error` logs.
1801
+ *
1802
+ * T3-TS-030 chargepoint denials surface as a typed
1803
+ * `InsufficientCreditError` so frontends can branch on
1804
+ * `instanceof` to render an "out of credit" UI without
1805
+ * prefix-matching the message themselves. Wire format is pinned
1806
+ * by `api/src/error.rs::service_insufficient_credit_wire_format_is_stable`.
1807
+ */
1808
+ private throwIfRpcError;
1809
+ private sendMultipartRpcRequest;
1630
1810
  /**
1631
1811
  * Capture the server-minted `Session-Id` from the last handshake
1632
1812
  * response headers (pentest M-1 / MAT-983). Validates shape so a