@terminal3/t3n-sdk 1.3.2 → 2.3.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.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * User-to-Agent Delegation (T3-TS-030).
3
+ *
4
+ * TypeScript reference implementation of the delegation credential and
5
+ * envelope shapes defined in `node/tee_contracts/delegation-types`.
6
+ *
7
+ * The wire shape is byte-for-byte identical to the Rust source — pinned
8
+ * by the KAT fixtures under `tests/kat/`. Specifically:
9
+ *
10
+ * - `not_before_secs` / `not_after_secs` / `batch_cap_cents` are
11
+ * emitted as **JSON strings** (e.g. `"1700086400"`) so JS Number
12
+ * precision never enters the canonicalisation surface.
13
+ * - `nonce` (16 B), `vc_id` (16 B), `request_hash` (32 B),
14
+ * `agent_pubkey` (33 B compressed secp256k1), `user_sig`,
15
+ * `agent_sig` are emitted as **base64url-no-pad** strings.
16
+ * - `org_did` / `user_did` are emitted as `did:t3n:<40-hex>` (the
17
+ * `CompactDid` `Display` form).
18
+ *
19
+ * Canonicalisation uses the npm `canonicalize` package (RFC 8785 JCS).
20
+ * Cryptography uses `@noble/curves` (secp256k1) and `@noble/hashes`
21
+ * (sha256, keccak_256).
22
+ */
23
+ /** Domain tag carried in `DelegationCredential.v`. */
24
+ export declare const DELEGATION_CREDENTIAL_DOMAIN: "ot3.delegation/1";
25
+ /** Domain tag prepended to the agent-side pre-image. */
26
+ export declare const DELEGATION_INVOCATION_DOMAIN: "ot3.invocation/1";
27
+ export declare const VC_ID_LEN = 16;
28
+ export declare const NONCE_LEN = 16;
29
+ export declare const REQUEST_HASH_LEN = 32;
30
+ export declare const AGENT_PUBKEY_LEN = 33;
31
+ export declare const ETH_SIG_LEN = 65;
32
+ export declare const MAX_CONTRACT_LEN = 46;
33
+ export declare const MAX_FUNCTION_LEN = 64;
34
+ export declare const MAX_SCOPE_LEN = 64;
35
+ export declare const MAX_SCOPES_PER_CREDENTIAL = 32;
36
+ export declare const MAX_METADATA_PER_CREDENTIAL = 16;
37
+ export declare const MAX_METADATA_KEY_LEN = 64;
38
+ export declare const MAX_METADATA_VALUE_LEN = 256;
39
+ /** User-to-agent delegation credential body. */
40
+ export interface DelegationCredential {
41
+ /** Domain tag, must equal {@link DELEGATION_CREDENTIAL_DOMAIN}. */
42
+ v: string;
43
+ /** `did:t3n:<40-hex>` user DID. */
44
+ user_did: string;
45
+ /** 33-byte compressed secp256k1 public key the agent uses per call. */
46
+ agent_pubkey: Uint8Array;
47
+ /** `did:t3n:<40-hex>` org DID. */
48
+ org_did: string;
49
+ /** Contract id, e.g. `"tee:payroll"`. */
50
+ contract: string;
51
+ /** Function name, e.g. `"run-payroll"`. */
52
+ function: string;
53
+ /** Org-data scopes the contract may read on this user's behalf. */
54
+ scopes: string[];
55
+ /** Flat key-value labels checked against the org grant. */
56
+ metadata: Record<string, string>;
57
+ /** Inclusive lower bound of the validity window (unix seconds). */
58
+ not_before_secs: bigint;
59
+ /** Inclusive upper bound of the validity window (unix seconds). */
60
+ not_after_secs: bigint;
61
+ /** Random 16-byte credential id. */
62
+ vc_id: Uint8Array;
63
+ }
64
+ /** Envelope wrapping a contract-specific request body. */
65
+ export interface DelegationEnvelope {
66
+ /** RFC 8785 JCS bytes of the credential, exactly as signed. */
67
+ credential_jcs: Uint8Array;
68
+ /** 65-byte EIP-191 signature over `credential_jcs`. */
69
+ user_sig: Uint8Array;
70
+ /** Per-call agent signature over the invocation pre-image. */
71
+ agent_sig: Uint8Array;
72
+ /** 16-byte agent-generated per-call nonce. */
73
+ nonce: Uint8Array;
74
+ /** SHA-256 of the canonical request body. */
75
+ request_hash: Uint8Array;
76
+ }
77
+ /** Payroll-specific request body wrapped by a delegation envelope. */
78
+ export interface PayrollRunRequest {
79
+ /** `did:t3n:<40-hex>` org id. */
80
+ org_id: string;
81
+ cycle_id: string;
82
+ pay_period_start: string;
83
+ pay_period_end: string;
84
+ /** Total cap on the run's net disbursement, in cents. */
85
+ batch_cap_cents: bigint;
86
+ /** `employee_id` → previous-cycle baseline net disbursement, cents (decimal string). */
87
+ historical_baselines: Record<string, string>;
88
+ }
89
+ /** Convenience wrapper paired with the matching delegation envelope. */
90
+ export interface PayrollInvocation {
91
+ envelope: DelegationEnvelope;
92
+ request: PayrollRunRequest;
93
+ }
94
+ /** Response from `tee:delegation.sign`. */
95
+ export interface SignDelegationResponse {
96
+ credential_jcs: Uint8Array;
97
+ user_sig: Uint8Array;
98
+ }
99
+ declare function b64uEncode(input: Uint8Array): string;
100
+ declare function b64uDecode(input: string): Uint8Array;
101
+ /**
102
+ * Encode raw bytes to base64url-no-pad (RFC 4648 §5 with padding
103
+ * dropped). The same encoding T3-TS-030 wire-shape uses for binary
104
+ * fields (`agent_pubkey`, `vc_id`, `nonce`, `agent_sig`, `user_sig`,
105
+ * `request_hash`, `credential_jcs`).
106
+ *
107
+ * Public API since v2.2: callers building `PayrollInvocation` JSON
108
+ * for the wire (e.g. the t3n-mcp `runPayroll` handler) need this
109
+ * encoder to match the contract's deserializer.
110
+ */
111
+ export declare function b64uEncodeBytes(input: Uint8Array): string;
112
+ /**
113
+ * Decode a base64url-no-pad string. Strict — rejects standard-alphabet
114
+ * `+` / `/` and any padding `=`.
115
+ */
116
+ export declare function b64uDecodeStrict(input: string): Uint8Array;
117
+ /** @internal — preserved alias for in-tree callers. Prefer
118
+ * {@link b64uEncodeBytes} / {@link b64uDecodeStrict}. */
119
+ export declare const _b64uEncode: typeof b64uEncode;
120
+ /** @internal — preserved alias for in-tree callers. Prefer
121
+ * {@link b64uEncodeBytes} / {@link b64uDecodeStrict}. */
122
+ export declare const _b64uDecode: typeof b64uDecode;
123
+ /** Build a `did:t3n:<40-hex>` from raw 20 bytes. */
124
+ export declare function compactDidFromBytes(bytes: Uint8Array): string;
125
+ /**
126
+ * Canonicalise a {@link DelegationCredential} to RFC 8785 JCS bytes.
127
+ *
128
+ * Output is byte-identical to the Rust `canonicalise_credential` in
129
+ * `delegation-types` (pinned by `tests/kat/credential.json`).
130
+ */
131
+ export declare function canonicaliseCredential(credential: DelegationCredential): Uint8Array;
132
+ /** Canonicalise an arbitrary {@link PayrollRunRequest} to JCS bytes. */
133
+ export declare function canonicaliseRequest(request: PayrollRunRequest): Uint8Array;
134
+ /** SHA-256 of the canonicalised request body. */
135
+ export declare function requestHash(request: PayrollRunRequest): Uint8Array;
136
+ /**
137
+ * Build the agent-side pre-image bytes:
138
+ * `utf8(DELEGATION_INVOCATION_DOMAIN) || vc_id || nonce || request_hash`.
139
+ *
140
+ * SHA-256 of these bytes is what the agent's secp256k1 signature is
141
+ * verified against.
142
+ */
143
+ export declare function buildInvocationPreimage(vcId: Uint8Array, nonce: Uint8Array, reqHash: Uint8Array): Uint8Array;
144
+ /** Options for {@link buildDelegationCredential}. */
145
+ export interface BuildDelegationCredentialOpts {
146
+ user_did: string;
147
+ agent_pubkey: Uint8Array;
148
+ org_did: string;
149
+ contract: string;
150
+ function: string;
151
+ scopes?: string[];
152
+ metadata?: Record<string, string>;
153
+ not_before_secs: bigint | number;
154
+ not_after_secs: bigint | number;
155
+ vc_id: Uint8Array;
156
+ }
157
+ /**
158
+ * Construct a {@link DelegationCredential} and validate body-level
159
+ * invariants (domain, lengths, validity window). Throws on the same
160
+ * conditions the Rust `validate_credential_body` rejects.
161
+ */
162
+ export declare function buildDelegationCredential(opts: BuildDelegationCredentialOpts): DelegationCredential;
163
+ /**
164
+ * Body-level validation matching `delegation-types::validate_credential_body`,
165
+ * minus the `now`/`max_validity_secs` checks (which are caller-supplied).
166
+ * Throws with a message identifying the offending invariant.
167
+ */
168
+ export declare function validateCredentialBody(c: DelegationCredential): void;
169
+ /** Compute the EIP-191 "personal_sign" digest of a message. */
170
+ export declare function eip191Digest(msg: Uint8Array): Uint8Array;
171
+ /**
172
+ * EIP-191 sign `jcs` under `secret`, returning a 65-byte signature
173
+ * (`r || s || v`, with `v` in 27/28 — Ethereum convention) and the
174
+ * recovered 20-byte ETH address.
175
+ */
176
+ export declare function signCredential(jcs: Uint8Array, secret: Uint8Array): {
177
+ sig: Uint8Array;
178
+ addr: Uint8Array;
179
+ };
180
+ /**
181
+ * Recover the 20-byte ETH address that signed `msg` under EIP-191.
182
+ * Mirrors `delegation-types::eth_recover_eip191`.
183
+ *
184
+ * **Signature malleability — accepted by design.** This routine does
185
+ * not enforce low-s. EIP-2 mandates low-s for *transaction* signatures,
186
+ * but EIP-191 / `personal_sign` has no such requirement, and ethers.js
187
+ * / MetaMask / web3.js produce both shapes. Two distinct `(r, s)` and
188
+ * `(r, n − s)` pairs verify under the same recovered address — replay
189
+ * protection comes from the envelope's `request_hash` + `nonce`, not
190
+ * from byte-uniqueness of the signature.
191
+ */
192
+ export declare function ethRecoverEip191(msg: Uint8Array, sig: Uint8Array): Uint8Array;
193
+ /**
194
+ * Sign the agent-side invocation pre-image. The signature is raw
195
+ * compact ECDSA (64 bytes) over `sha256(preimage)` — what
196
+ * `delegation-types::verify_agent_sig` accepts as the 64-byte form.
197
+ */
198
+ export declare function signAgentInvocation(preimage: Uint8Array, secret: Uint8Array): Uint8Array;
199
+ /** Options for {@link buildPayrollInvocation}. */
200
+ export interface BuildPayrollInvocationOpts {
201
+ credentialJcs: Uint8Array;
202
+ userSig: Uint8Array;
203
+ /** Credential's `vc_id` — needed for the agent pre-image. */
204
+ vcId: Uint8Array;
205
+ nonce: Uint8Array;
206
+ request: PayrollRunRequest;
207
+ agentSecret: Uint8Array;
208
+ }
209
+ /**
210
+ * Assemble a complete {@link PayrollInvocation} (envelope + request)
211
+ * given a user-signed credential and a per-call agent secret. Computes
212
+ * `request_hash` from the canonical request bytes and produces an
213
+ * `agent_sig` over `sha256(invocation_preimage)`.
214
+ */
215
+ export declare function buildPayrollInvocation(opts: BuildPayrollInvocationOpts): PayrollInvocation;
216
+ export {};
@@ -9,3 +9,4 @@ export * from "./encryption";
9
9
  export * from "./actions";
10
10
  export * from "./request-parser";
11
11
  export * from "./contract-response";
12
+ export * from "./delegation";
@@ -8,6 +8,7 @@ import { T3nClientConfig } from "./config";
8
8
  import { type ContractResponseSchema } from "./contract-response";
9
9
  import { SessionId, Did, SessionStatus, AuthInput, HandshakeResult } from "../types";
10
10
  import { KycPollOptions, KycStatus } from "../types/kyc";
11
+ import { OtpChannel, OtpRequestInput, OtpRequestResult, OtpVerifyInput, OtpVerifyResult, SubmitUserInputArgs, SubmitUserInputResult } from "../types/user";
11
12
  /**
12
13
  * Main T3n SDK Client
13
14
  */
@@ -240,6 +241,116 @@ export declare class T3nClient {
240
241
  * row exists yet), or if the response shape is unexpected.
241
242
  */
242
243
  kycStatus(providerId?: string): Promise<KycStatus>;
244
+ /**
245
+ * Dispatch a one-time code to the supplied contact via the host's
246
+ * OTP provider. Backed by `tee:user/contracts::otp-request`.
247
+ *
248
+ * The contract persists the unverified contact in the channel's
249
+ * pending slot (`pending_email` / `pending_phone`) and returns
250
+ * `OtpRequestResult` with `status = "otp_pending"` (or `undefined`
251
+ * when the node is configured with `skip_otp = true`). The next
252
+ * step is {@link otpVerify} with the code the user typed.
253
+ *
254
+ * Behaviour notes:
255
+ *
256
+ * - Contact is a discriminated object: `emailChannel` or
257
+ * `smsChannel` (mirrors Rust `OtpRequest`). The legacy
258
+ * `keys.generic_api.otp_channel` body shadow is rejected by the
259
+ * contract.
260
+ * - SMS channel is gated on a verified email. Calling with
261
+ * `channel = "sms"` against a DID that hasn't completed an
262
+ * email roundtrip first throws.
263
+ * - On a fresh DID's first call, `result.isNewProfile === true`.
264
+ * Use this signal — not [[otpVerify]]'s response — for "did the
265
+ * user just register" gating.
266
+ *
267
+ * @throws {RpcError} if the node rejects the action; the message
268
+ * carries the contract-side detail (e.g. sequential-flow guard
269
+ * when the email is missing).
270
+ */
271
+ otpRequest(input: OtpRequestInput): Promise<OtpRequestResult>;
272
+ /**
273
+ * Redeem a one-time code and bind the contact to the
274
+ * authenticated DID. Backed by
275
+ * `tee:user/contracts::otp-verify`.
276
+ *
277
+ * On success the contract promotes the pending contact back to
278
+ * the canonical slot, writes the AUTH_MAP / DIDS_MAP /
279
+ * USER_AUTHS_MAP authenticator entries, stamps
280
+ * `verified_contacts.{email,phone}`, and mints the ambient
281
+ * `t3n.personal.contact.1` ownership VC when both contacts are
282
+ * now verified.
283
+ *
284
+ * If the contact is already owned by a different DID the
285
+ * contract refuses to silently reparent the authenticator and
286
+ * surfaces a `mergeSuggestion` instead — the caller resolves the
287
+ * merge (typically via {@link mergeProfiles}) and re-attempts.
288
+ *
289
+ * On a wrong / expired code the contract returns the result with
290
+ * `status = "otp_failed"` or `"otp_expired"` — no exception is
291
+ * thrown, the error is surfaced as data so the UI can stay on
292
+ * the verify screen and let the user retry.
293
+ *
294
+ * @throws {RpcError} if the node rejects the action outright
295
+ * (network / decode / bad input shape). Branch on
296
+ * `result.status` for retryable OTP failures.
297
+ */
298
+ otpVerify(input: OtpVerifyInput): Promise<OtpVerifyResult>;
299
+ /**
300
+ * Submit Level-1 user-input fields to the slim
301
+ * `tee:user/contracts::user-upsert`. The contract merges the
302
+ * supplied profile fields, validates them, mints
303
+ * `t3n.user-input.kyc.1` once every Level-1 field is present, and
304
+ * commits the write.
305
+ *
306
+ * **Pre-condition** (MAT-1374): the DID must already have a
307
+ * verified email — either because {@link otpVerify} bound one or
308
+ * because the session carries a proving authenticator (OIDC /
309
+ * Email auth). Calls without proof are rejected with
310
+ * {@link UserUpsertError} `kind = "EmailNotVerified"`. The
311
+ * recommended UX is "request OTP -> verify OTP -> submit user
312
+ * input" (or use {@link runOtpThenUserInput} which chains all
313
+ * three).
314
+ *
315
+ * The KYC webhook orphan-attestation flow stays here: when
316
+ * `requireExistingUser` is set, the contract identifies the user
317
+ * by DID (vendorData) instead of email and the gate is bypassed.
318
+ *
319
+ * @throws {UserUpsertError} when the contract returns a typed
320
+ * error (`email_not_verified`, `legacy_field`, `user_not_found`).
321
+ * Branch on `err.kind`.
322
+ * @throws {RpcError} for non-typed transport / decode failures.
323
+ */
324
+ submitUserInput(input: SubmitUserInputArgs): Promise<SubmitUserInputResult>;
325
+ /**
326
+ * Convenience helper: run the explicit OTP roundtrip and submit
327
+ * the slim user-upsert in one call.
328
+ *
329
+ * The caller supplies a `getOtpCode(contact, channel)` callback
330
+ * the SDK invokes between request and verify — this is where
331
+ * a UI prompts the user to type the code that arrived on email
332
+ * / SMS. Throw from the callback to abort the flow; the helper
333
+ * does not retry on its own.
334
+ *
335
+ * Returns the slim `submitUserInput` result on success. Throws
336
+ * {@link UserUpsertError} or `RpcError` on the same conditions
337
+ * as the underlying wrappers.
338
+ *
339
+ * Optional, opt-in path — the recommended default is to call
340
+ * {@link otpRequest}, {@link otpVerify}, and
341
+ * {@link submitUserInput} explicitly so the application owns the
342
+ * flow.
343
+ */
344
+ runOtpThenUserInput(args: {
345
+ channel: OtpChannel;
346
+ emailAddress?: string;
347
+ phoneNumber?: string;
348
+ profile: SubmitUserInputArgs["profile"];
349
+ organisationDid?: string;
350
+ attestations?: unknown;
351
+ keys?: Record<string, unknown>;
352
+ getOtpCode: (contact: string, channel: OtpChannel) => Promise<string> | string;
353
+ }): Promise<SubmitUserInputResult>;
243
354
  /**
244
355
  * Poll `kyc-status` until a terminal status arrives or the
245
356
  * configured timeout elapses.
@@ -18,6 +18,10 @@ export type { SessionId, Did, OidcCredentials, AuthInput, EthAuthInput, OidcAuth
18
18
  export { SessionStatus, AuthMethod, createEthAuthInput, createOidcAuthInput, } from "./types";
19
19
  export type { KycStatus, KycStatusKind, KycPollOptions, KycPollCadence, } from "./types/kyc";
20
20
  export { DEFAULT_KYC_POLL_CADENCE, TERMINAL_KYC_STATUSES, KycStatusTimeoutError, } from "./types/kyc";
21
+ export type { OtpChannel, OtpRequestInput, OtpRequestResult, OtpVerifyInput, OtpVerifyResult, OtpMergeSuggestion, UserInputProfile, SubmitUserInputArgs, SubmitUserInputResult, UserUpsertErrorKind, } from "./types/user";
22
+ export { UserUpsertError } from "./types/user";
23
+ export { DELEGATION_CREDENTIAL_DOMAIN, DELEGATION_INVOCATION_DOMAIN, VC_ID_LEN, NONCE_LEN, REQUEST_HASH_LEN, AGENT_PUBKEY_LEN, ETH_SIG_LEN, buildDelegationCredential, validateCredentialBody, canonicaliseCredential, canonicaliseRequest, requestHash, buildInvocationPreimage, eip191Digest, signCredential, ethRecoverEip191, signAgentInvocation, buildPayrollInvocation, compactDidFromBytes, b64uEncodeBytes, b64uDecodeStrict, _b64uEncode, } from "./client/delegation";
24
+ export type { DelegationCredential, DelegationEnvelope, PayrollRunRequest, PayrollInvocation, SignDelegationResponse, BuildDelegationCredentialOpts, BuildPayrollInvocationOpts, } from "./client/delegation";
21
25
  export { metamask_sign, metamask_get_address, eth_get_address, createDefaultHandlers, createMlKemPublicKeyHandler, createRandomHandler, } from "./client/handlers";
22
26
  export type { WasmComponent, ClientHandshake, ClientAuth, SessionCrypto, WasmNextResult, } from "./wasm";
23
27
  export { loadWasmComponent } from "./wasm";
@@ -40,3 +40,4 @@ export interface GuestToHostHandlers {
40
40
  }
41
41
  export * from "./session";
42
42
  export * from "./auth";
43
+ export * from "./user";
@@ -0,0 +1,194 @@
1
+ /**
2
+ * MAT-1374 — wire types for the explicit `otp-request` /
3
+ * `otp-verify` / slim `user-upsert` exports on `tee:user/contracts`
4
+ * (`tee:user@2.0.0`).
5
+ *
6
+ * Pre-2.0.0 the user contract dispatched OTP request and OTP verify
7
+ * implicitly from the input shape of a single `user-upsert` call.
8
+ * 2.0.0 splits that surface into three explicit functions; the
9
+ * shapes here mirror the Rust types in
10
+ * `node/tee_contracts/user/src/otp_types.rs` and
11
+ * `node/tee_contracts/user/src/upsert_types.rs`.
12
+ *
13
+ * Keep the two in sync — bytes flow directly from the contract
14
+ * through the JSON-RPC envelope into the SDK wrappers
15
+ * (`T3nClient.otpRequest`, `T3nClient.otpVerify`,
16
+ * `T3nClient.submitUserInput`).
17
+ */
18
+ import { T3nError } from "../utils/errors";
19
+ /**
20
+ * OTP delivery channel. Matches the `OtpContactChannel` Rust enum
21
+ * with `serde(rename_all = "snake_case")`.
22
+ */
23
+ export type OtpChannel = "email" | "sms";
24
+ /**
25
+ * Discriminated OTP contact target. Mirrors the Rust `OtpRequest` enum
26
+ * (`email_channel` / `sms_channel` on the wire). Exactly one branch is
27
+ * set — no parallel optional email + phone with a separate channel tag.
28
+ *
29
+ * The legacy `keys.generic_api.otp_channel` body shadow was removed
30
+ * in 2.0.0 — the contract rejects calls carrying it with a
31
+ * `legacy_field` error.
32
+ */
33
+ export type OtpRequestInput = {
34
+ emailChannel: {
35
+ emailAddress: string;
36
+ };
37
+ } | {
38
+ smsChannel: {
39
+ phoneNumber: string;
40
+ };
41
+ };
42
+ /**
43
+ * Response returned by `otp-request`. Mirrors `OtpResponse` in
44
+ * `otp_types.rs`.
45
+ *
46
+ * - `contact` echoes the OTP destination so clients that race
47
+ * multiple channels can correlate replies.
48
+ * - `expiresAtSec` is the Unix-second TTL the host minted for the
49
+ * pending OTP. Absent in `skip_otp` test environments.
50
+ * - `status` is `"otp_pending"` on the happy path. `"otp_failed"`
51
+ * only appears on retry without a fresh request — usually you
52
+ * shouldn't see it on `otp-request`.
53
+ * - `txHash` is the host-enriched ledger ref for the OTP-pending
54
+ * write (the contract leaves it `null`; the host injects).
55
+ * - `isNewProfile` is `true` on a fresh DID's first OTP request —
56
+ * read this to detect "did the user just register".
57
+ */
58
+ export interface OtpRequestResult {
59
+ contact: string;
60
+ channel: OtpChannel;
61
+ expiresAtSec?: number;
62
+ status?: string;
63
+ txHash?: string;
64
+ isNewProfile?: boolean;
65
+ }
66
+ /**
67
+ * Input to `T3nClient.otpVerify`. `otpCode` is mandatory; `request`
68
+ * repeats the same discriminated contact shape as {@link OtpRequestInput}.
69
+ */
70
+ export interface OtpVerifyInput {
71
+ otpCode: string;
72
+ request: OtpRequestInput;
73
+ }
74
+ /**
75
+ * Suggestion surfaced when the bind-target contact already belongs
76
+ * to another DID. The contract refuses to silently steal the
77
+ * authenticator; the caller must resolve the merge (typically via
78
+ * `merge-profiles`) before re-attempting verify.
79
+ */
80
+ export interface OtpMergeSuggestion {
81
+ existingDid: string;
82
+ currentDid: string;
83
+ email?: string;
84
+ phone?: string;
85
+ channel: OtpChannel;
86
+ }
87
+ /**
88
+ * Response returned by `otp-verify`. Mirrors
89
+ * `OtpVerifyResponse` in `otp_types.rs`.
90
+ *
91
+ * - `email` is populated on `channel = "email"` success;
92
+ * `phone` on `channel = "sms"` success. Mutually exclusive on
93
+ * the happy path.
94
+ * - `status` carries `"otp_failed"` / `"otp_expired"` on retry; the
95
+ * happy path leaves it `undefined`.
96
+ * - `mergeSuggestion` is set when the contact-to-bind is already
97
+ * owned by another DID. Wire it into your merge UX.
98
+ */
99
+ export interface OtpVerifyResult {
100
+ txHash?: string;
101
+ did: string;
102
+ channel: OtpChannel;
103
+ email?: string;
104
+ phone?: string;
105
+ status?: string;
106
+ mergeSuggestion?: OtpMergeSuggestion;
107
+ isNewProfile?: boolean;
108
+ }
109
+ /**
110
+ * Level-1 user-input fields accepted by the slim `user-upsert`.
111
+ * The Rust contract validates these via `validate_profile`; the
112
+ * shape is intentionally open so callers can add provider-specific
113
+ * fields on top of the canonical six.
114
+ */
115
+ export interface UserInputProfile {
116
+ firstName?: string;
117
+ lastName?: string;
118
+ email_address?: string;
119
+ phone_number?: string;
120
+ birthdate?: string;
121
+ nationality?: string;
122
+ [key: string]: unknown;
123
+ }
124
+ /**
125
+ * Input to `T3nClient.submitUserInput`. Maps onto the slim
126
+ * `user-upsert` request shape: `{ profile, organisation_did?,
127
+ * attestations?, keys? }`.
128
+ */
129
+ export interface SubmitUserInputArgs {
130
+ profile: UserInputProfile;
131
+ organisationDid?: string;
132
+ attestations?: unknown;
133
+ keys?: Record<string, unknown>;
134
+ /**
135
+ * KYC webhook orphan-attestation flow. When set, the contract
136
+ * skips the email-not-verified gate and only records the
137
+ * attestation if the DID has no profile yet (T3-TS-026 §13).
138
+ * Most callers should leave this `undefined`.
139
+ */
140
+ requireExistingUser?: boolean;
141
+ }
142
+ /**
143
+ * Response shape returned by the slim `user-upsert`. Carries the
144
+ * post-merge tx hash plus the L1 merge diagnostics
145
+ * (`refusedFields`, `mergeSuggestion`) the pre-2.0.0 omnibus
146
+ * `UpsertResponse` already surfaced.
147
+ */
148
+ export interface SubmitUserInputResult {
149
+ txHash?: string;
150
+ refusedFields?: string[];
151
+ mergeSuggestion?: OtpMergeSuggestion;
152
+ userFound?: boolean;
153
+ }
154
+ /**
155
+ * Discriminator for {@link UserUpsertError}. Mirrors the
156
+ * `UserUpsertError` Rust enum and its `<code>:<detail>` wire
157
+ * format (see `upsert_types.rs::UserUpsertError::code`). Branch
158
+ * with a `switch` over `kind`.
159
+ *
160
+ * - `EmailNotVerified` — the slim `user-upsert` was called against
161
+ * a DID that has no verified email and no proving authenticator.
162
+ * Run `otpRequest` + `otpVerify` first (or accept an
163
+ * OIDC/Email-authed session).
164
+ * - `LegacyField` — caller passed a pre-2.0.0 dispatch field
165
+ * (`otp_code` to a non-verify export, or
166
+ * `keys.generic_api.otp_channel` anywhere). Migrate the call
167
+ * site to the new explicit functions.
168
+ * - `UserNotFound` — `requireExistingUser` was set but no profile
169
+ * exists for the DID. The attestation is recorded for audit;
170
+ * no profile created.
171
+ */
172
+ export type UserUpsertErrorKind = "EmailNotVerified" | "LegacyField" | "UserNotFound";
173
+ /**
174
+ * Typed wrapper for the `<code>:<detail>` errors the slim
175
+ * `user-upsert` and the OTP entry points emit. `kind` is the
176
+ * structured discriminator the SDK derives from the error code
177
+ * prefix; `code` and `detail` retain the wire components for
178
+ * fall-through logging.
179
+ *
180
+ * Throw site: `T3nClient.submitUserInput` (and friends) when the
181
+ * contract returns a string error matching the
182
+ * `<code>:<detail>` shape.
183
+ */
184
+ export declare class UserUpsertError extends T3nError {
185
+ readonly kind: UserUpsertErrorKind | "Unknown";
186
+ readonly detail: string;
187
+ constructor(code: string, detail: string);
188
+ /**
189
+ * Try to parse a contract error string into a `UserUpsertError`.
190
+ * Returns `null` if `raw` doesn't match the `<code>:<detail>`
191
+ * shape — caller falls back to a generic error.
192
+ */
193
+ static fromWire(raw: string): UserUpsertError | null;
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terminal3/t3n-sdk",
3
- "version": "1.3.2",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "T3n TypeScript SDK - A minimal SDK that mirrors the server's RPC handler approach",
6
6
  "main": "dist/index.js",
@@ -101,6 +101,9 @@
101
101
  "dependencies": {
102
102
  "@bytecodealliance/jco": "^1.17.6",
103
103
  "@bytecodealliance/preview2-shim": "^0.17.8",
104
+ "@noble/curves": "^2.2.0",
105
+ "@noble/hashes": "^2.2.0",
106
+ "canonicalize": "^3.0.0",
104
107
  "ethers": "^6.16.0"
105
108
  },
106
109
  "pnpm": {