@terminal3/t3n-sdk 1.3.2 → 2.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/README.md CHANGED
@@ -86,6 +86,88 @@ const oidcCredentials = {
86
86
  const did = await client.authenticate(createOidcAuthInput(oidcCredentials));
87
87
  ```
88
88
 
89
+ ## Migrating from 1.x
90
+
91
+ `@terminal3/t3n-sdk@2.0.0` cuts over to `tee:user/contracts@2.0.0`
92
+ (MAT-1374). The implicit-dispatch monolith `user-upsert` on the user
93
+ contract was split into three explicit functions on the same
94
+ contract:
95
+
96
+ - `otp-request` — request + dispatch an OTP code.
97
+ - `otp-verify` — redeem an OTP and bind the contact.
98
+ - `user-upsert` (slim) — Level 1 user-input ingest only. Rejects
99
+ callers without a verified email with the typed
100
+ `UserUpsertError { kind: "EmailNotVerified" }` (wire form
101
+ `email_not_verified:<detail>`).
102
+
103
+ If you used the typed `T3nClient` methods to wrap `executeAction`,
104
+ the SDK now ships `client.otpRequest` / `client.otpVerify` /
105
+ `client.submitUserInput` (plus a convenience
106
+ `client.runOtpThenUserInput`) so you can migrate one call site at a
107
+ time:
108
+
109
+ | Pre-2.0.0 (`tee:user@1.5.0`) | 2.x (`tee:user@2.0.0`, contract ≥ 2.1.0 for discriminated OTP) |
110
+ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
111
+ | `executeAction({ function_name: "user-upsert", input: { profile: { email_address } } })` | `client.otpRequest({ emailChannel: { emailAddress } })` |
112
+ | `executeAction({ function_name: "user-upsert", input: { profile, otp_code } })` | `client.otpVerify({ otpCode, request: { emailChannel: { emailAddress } } })` |
113
+ | `executeAction({ function_name: "user-upsert", input: { profile, keys: { generic_api: { otp_channel: "sms" } } } })` | `client.otpRequest({ smsChannel: { phoneNumber } })` |
114
+ | `executeAction({ function_name: "user-upsert", input: { profile } })` (post-OTP) | `client.submitUserInput({ profile })` |
115
+
116
+ ### Worked example: full email + L1 ingest
117
+
118
+ ```typescript
119
+ import { T3nClient, UserUpsertError } from "@terminal3/t3n-sdk";
120
+
121
+ // 1) Bind the user's email via OTP.
122
+ const requested = await client.otpRequest({
123
+ emailChannel: { emailAddress: "alice@example.com" },
124
+ });
125
+ const code = await prompt(`Code sent to ${requested.contact}: `);
126
+ await client.otpVerify({
127
+ otpCode: code,
128
+ request: { emailChannel: { emailAddress: "alice@example.com" } },
129
+ });
130
+
131
+ // 2) Slim user-upsert: Level 1 user-input ingest. Rejects with
132
+ // UserUpsertError(kind: "EmailNotVerified") if step 1 was skipped.
133
+ try {
134
+ const result = await client.submitUserInput({
135
+ profile: {
136
+ first_name: "Alice",
137
+ last_name: "Smith",
138
+ country_of_residence: "US",
139
+ // ...other Level-1 fields
140
+ },
141
+ });
142
+ console.log("tx:", result.txHash);
143
+ } catch (err) {
144
+ if (err instanceof UserUpsertError && err.kind === "EmailNotVerified") {
145
+ // run otp-request + otp-verify, then retry.
146
+ }
147
+ throw err;
148
+ }
149
+ ```
150
+
151
+ For tests that "just want it to work", `runOtpThenUserInput` chains
152
+ the three calls behind a single `getOtpCode` callback.
153
+
154
+ ### Hard-error fields
155
+
156
+ Passing any of these to the wrong function returns the typed
157
+ `UserUpsertError(kind: "LegacyField")` (wire form
158
+ `legacy_field:<detail>`):
159
+
160
+ - `otp_code` to anything other than `otp-verify`.
161
+ - `keys.generic_api.otp_channel` to anything (channel is now a
162
+ top-level field).
163
+
164
+ ### Raw `executeAction` callers
165
+
166
+ If you bypass the typed wrappers, see the migration table above:
167
+ the function names (`otp-request` / `otp-verify` / `user-upsert`)
168
+ and JSON input shapes line up 1:1 with the wrappers' camelCase
169
+ fields converted to `snake_case`.
170
+
89
171
  ## Architecture
90
172
 
91
173
  The T3n SDK follows the same architectural principles as the server's `rpc.rs`:
package/dist/index.d.ts CHANGED
@@ -325,6 +325,281 @@ type AuthInput = EthAuthInput | OidcAuthInput;
325
325
  declare function createEthAuthInput(address: string): EthAuthInput;
326
326
  declare function createOidcAuthInput(credentials: OidcCredentials): OidcAuthInput;
327
327
 
328
+ /**
329
+ * Error classes for T3n SDK
330
+ */
331
+ /**
332
+ * Base error class for T3n SDK errors
333
+ */
334
+ declare class T3nError extends Error {
335
+ readonly code?: string | undefined;
336
+ constructor(message: string, code?: string | undefined);
337
+ }
338
+ /**
339
+ * Error thrown when session is in invalid state for operation
340
+ */
341
+ declare class SessionStateError extends T3nError {
342
+ readonly currentState: string;
343
+ constructor(message: string, currentState: string);
344
+ }
345
+ /**
346
+ * Error thrown during authentication process
347
+ */
348
+ declare class AuthenticationError extends T3nError {
349
+ readonly authMethod?: string | undefined;
350
+ constructor(message: string, authMethod?: string | undefined);
351
+ }
352
+ /**
353
+ * Error thrown during handshake process
354
+ */
355
+ declare class HandshakeError extends T3nError {
356
+ constructor(message: string);
357
+ }
358
+ /**
359
+ * Error thrown during RPC communication.
360
+ *
361
+ * `message` is the human-readable error (preferring the server's
362
+ * `error.data.detail` when present, with the request id appended in
363
+ * `[<id>]` form so a UI that only surfaces `.message` still gives an
364
+ * operator something to grep). The structured fields below preserve
365
+ * the same info for callers that want to render or log them
366
+ * separately — e.g. a toast that shows `detail` but surfaces
367
+ * `requestId` in a "copy for support" affordance.
368
+ */
369
+ declare class RpcError extends T3nError {
370
+ readonly rpcMethod?: string | undefined;
371
+ readonly httpStatus?: number | undefined;
372
+ /** Server-attached detail (JSON-RPC `error.data.detail`). User-facing kinds
373
+ * carry the specific reason here; internal kinds omit it. */
374
+ readonly detail?: string | undefined;
375
+ /** Per-request correlation id (JSON-RPC `error.data.request_id`). */
376
+ readonly requestId?: string | undefined;
377
+ constructor(message: string, rpcMethod?: string | undefined, httpStatus?: number | undefined,
378
+ /** Server-attached detail (JSON-RPC `error.data.detail`). User-facing kinds
379
+ * carry the specific reason here; internal kinds omit it. */
380
+ detail?: string | undefined,
381
+ /** Per-request correlation id (JSON-RPC `error.data.request_id`). */
382
+ requestId?: string | undefined);
383
+ }
384
+ /**
385
+ * Error thrown when WASM operations fail
386
+ */
387
+ declare class WasmError extends T3nError {
388
+ readonly operation?: string | undefined;
389
+ readonly payload?: unknown | undefined;
390
+ constructor(message: string, operation?: string | undefined, payload?: unknown | undefined);
391
+ }
392
+ /**
393
+ * Decode WASM error message from comma-separated byte array format
394
+ * WASM errors often come as "83,101,114,100,101..." which represents ASCII bytes
395
+ *
396
+ * @param errorMessage - The error message string that may contain comma-separated bytes
397
+ * @returns Decoded error message if it was encoded, otherwise original message
398
+ */
399
+ declare function decodeWasmErrorMessage(errorMessage: string): string;
400
+ /**
401
+ * Extract and decode error from WASM ComponentError
402
+ *
403
+ * @param error - The error object from WASM
404
+ * @returns Decoded error message
405
+ */
406
+ declare function extractWasmError(error: unknown): string;
407
+
408
+ /**
409
+ * MAT-1374 — wire types for the explicit `otp-request` /
410
+ * `otp-verify` / slim `user-upsert` exports on `tee:user/contracts`
411
+ * (`tee:user@2.0.0`).
412
+ *
413
+ * Pre-2.0.0 the user contract dispatched OTP request and OTP verify
414
+ * implicitly from the input shape of a single `user-upsert` call.
415
+ * 2.0.0 splits that surface into three explicit functions; the
416
+ * shapes here mirror the Rust types in
417
+ * `node/tee_contracts/user/src/otp_types.rs` and
418
+ * `node/tee_contracts/user/src/upsert_types.rs`.
419
+ *
420
+ * Keep the two in sync — bytes flow directly from the contract
421
+ * through the JSON-RPC envelope into the SDK wrappers
422
+ * (`T3nClient.otpRequest`, `T3nClient.otpVerify`,
423
+ * `T3nClient.submitUserInput`).
424
+ */
425
+
426
+ /**
427
+ * OTP delivery channel. Matches the `OtpContactChannel` Rust enum
428
+ * with `serde(rename_all = "snake_case")`.
429
+ */
430
+ type OtpChannel = "email" | "sms";
431
+ /**
432
+ * Discriminated OTP contact target. Mirrors the Rust `OtpRequest` enum
433
+ * (`email_channel` / `sms_channel` on the wire). Exactly one branch is
434
+ * set — no parallel optional email + phone with a separate channel tag.
435
+ *
436
+ * The legacy `keys.generic_api.otp_channel` body shadow was removed
437
+ * in 2.0.0 — the contract rejects calls carrying it with a
438
+ * `legacy_field` error.
439
+ */
440
+ type OtpRequestInput = {
441
+ emailChannel: {
442
+ emailAddress: string;
443
+ };
444
+ } | {
445
+ smsChannel: {
446
+ phoneNumber: string;
447
+ };
448
+ };
449
+ /**
450
+ * Response returned by `otp-request`. Mirrors `OtpResponse` in
451
+ * `otp_types.rs`.
452
+ *
453
+ * - `contact` echoes the OTP destination so clients that race
454
+ * multiple channels can correlate replies.
455
+ * - `expiresAtSec` is the Unix-second TTL the host minted for the
456
+ * pending OTP. Absent in `skip_otp` test environments.
457
+ * - `status` is `"otp_pending"` on the happy path. `"otp_failed"`
458
+ * only appears on retry without a fresh request — usually you
459
+ * shouldn't see it on `otp-request`.
460
+ * - `txHash` is the host-enriched ledger ref for the OTP-pending
461
+ * write (the contract leaves it `null`; the host injects).
462
+ * - `isNewProfile` is `true` on a fresh DID's first OTP request —
463
+ * read this to detect "did the user just register".
464
+ */
465
+ interface OtpRequestResult {
466
+ contact: string;
467
+ channel: OtpChannel;
468
+ expiresAtSec?: number;
469
+ status?: string;
470
+ txHash?: string;
471
+ isNewProfile?: boolean;
472
+ }
473
+ /**
474
+ * Input to `T3nClient.otpVerify`. `otpCode` is mandatory; `request`
475
+ * repeats the same discriminated contact shape as {@link OtpRequestInput}.
476
+ */
477
+ interface OtpVerifyInput {
478
+ otpCode: string;
479
+ request: OtpRequestInput;
480
+ }
481
+ /**
482
+ * Suggestion surfaced when the bind-target contact already belongs
483
+ * to another DID. The contract refuses to silently steal the
484
+ * authenticator; the caller must resolve the merge (typically via
485
+ * `merge-profiles`) before re-attempting verify.
486
+ */
487
+ interface OtpMergeSuggestion {
488
+ existingDid: string;
489
+ currentDid: string;
490
+ email?: string;
491
+ phone?: string;
492
+ channel: OtpChannel;
493
+ }
494
+ /**
495
+ * Response returned by `otp-verify`. Mirrors
496
+ * `OtpVerifyResponse` in `otp_types.rs`.
497
+ *
498
+ * - `email` is populated on `channel = "email"` success;
499
+ * `phone` on `channel = "sms"` success. Mutually exclusive on
500
+ * the happy path.
501
+ * - `status` carries `"otp_failed"` / `"otp_expired"` on retry; the
502
+ * happy path leaves it `undefined`.
503
+ * - `mergeSuggestion` is set when the contact-to-bind is already
504
+ * owned by another DID. Wire it into your merge UX.
505
+ */
506
+ interface OtpVerifyResult {
507
+ txHash?: string;
508
+ did: string;
509
+ channel: OtpChannel;
510
+ email?: string;
511
+ phone?: string;
512
+ status?: string;
513
+ mergeSuggestion?: OtpMergeSuggestion;
514
+ isNewProfile?: boolean;
515
+ }
516
+ /**
517
+ * Level-1 user-input fields accepted by the slim `user-upsert`.
518
+ * The Rust contract validates these via `validate_profile`; the
519
+ * shape is intentionally open so callers can add provider-specific
520
+ * fields on top of the canonical six.
521
+ */
522
+ interface UserInputProfile {
523
+ firstName?: string;
524
+ lastName?: string;
525
+ email_address?: string;
526
+ phone_number?: string;
527
+ birthdate?: string;
528
+ nationality?: string;
529
+ [key: string]: unknown;
530
+ }
531
+ /**
532
+ * Input to `T3nClient.submitUserInput`. Maps onto the slim
533
+ * `user-upsert` request shape: `{ profile, organisation_did?,
534
+ * attestations?, keys? }`.
535
+ */
536
+ interface SubmitUserInputArgs {
537
+ profile: UserInputProfile;
538
+ organisationDid?: string;
539
+ attestations?: unknown;
540
+ keys?: Record<string, unknown>;
541
+ /**
542
+ * KYC webhook orphan-attestation flow. When set, the contract
543
+ * skips the email-not-verified gate and only records the
544
+ * attestation if the DID has no profile yet (T3-TS-026 §13).
545
+ * Most callers should leave this `undefined`.
546
+ */
547
+ requireExistingUser?: boolean;
548
+ }
549
+ /**
550
+ * Response shape returned by the slim `user-upsert`. Carries the
551
+ * post-merge tx hash plus the L1 merge diagnostics
552
+ * (`refusedFields`, `mergeSuggestion`) the pre-2.0.0 omnibus
553
+ * `UpsertResponse` already surfaced.
554
+ */
555
+ interface SubmitUserInputResult {
556
+ txHash?: string;
557
+ refusedFields?: string[];
558
+ mergeSuggestion?: OtpMergeSuggestion;
559
+ userFound?: boolean;
560
+ }
561
+ /**
562
+ * Discriminator for {@link UserUpsertError}. Mirrors the
563
+ * `UserUpsertError` Rust enum and its `<code>:<detail>` wire
564
+ * format (see `upsert_types.rs::UserUpsertError::code`). Branch
565
+ * with a `switch` over `kind`.
566
+ *
567
+ * - `EmailNotVerified` — the slim `user-upsert` was called against
568
+ * a DID that has no verified email and no proving authenticator.
569
+ * Run `otpRequest` + `otpVerify` first (or accept an
570
+ * OIDC/Email-authed session).
571
+ * - `LegacyField` — caller passed a pre-2.0.0 dispatch field
572
+ * (`otp_code` to a non-verify export, or
573
+ * `keys.generic_api.otp_channel` anywhere). Migrate the call
574
+ * site to the new explicit functions.
575
+ * - `UserNotFound` — `requireExistingUser` was set but no profile
576
+ * exists for the DID. The attestation is recorded for audit;
577
+ * no profile created.
578
+ */
579
+ type UserUpsertErrorKind = "EmailNotVerified" | "LegacyField" | "UserNotFound";
580
+ /**
581
+ * Typed wrapper for the `<code>:<detail>` errors the slim
582
+ * `user-upsert` and the OTP entry points emit. `kind` is the
583
+ * structured discriminator the SDK derives from the error code
584
+ * prefix; `code` and `detail` retain the wire components for
585
+ * fall-through logging.
586
+ *
587
+ * Throw site: `T3nClient.submitUserInput` (and friends) when the
588
+ * contract returns a string error matching the
589
+ * `<code>:<detail>` shape.
590
+ */
591
+ declare class UserUpsertError extends T3nError {
592
+ readonly kind: UserUpsertErrorKind | "Unknown";
593
+ readonly detail: string;
594
+ constructor(code: string, detail: string);
595
+ /**
596
+ * Try to parse a contract error string into a `UserUpsertError`.
597
+ * Returns `null` if `raw` doesn't match the `<code>:<detail>`
598
+ * shape — caller falls back to a generic error.
599
+ */
600
+ static fromWire(raw: string): UserUpsertError | null;
601
+ }
602
+
328
603
  /**
329
604
  * Public types export for T3n SDK
330
605
  */
@@ -515,86 +790,6 @@ interface T3nClientConfig {
515
790
  handlers?: GuestToHostHandlers;
516
791
  }
517
792
 
518
- /**
519
- * Error classes for T3n SDK
520
- */
521
- /**
522
- * Base error class for T3n SDK errors
523
- */
524
- declare class T3nError extends Error {
525
- readonly code?: string | undefined;
526
- constructor(message: string, code?: string | undefined);
527
- }
528
- /**
529
- * Error thrown when session is in invalid state for operation
530
- */
531
- declare class SessionStateError extends T3nError {
532
- readonly currentState: string;
533
- constructor(message: string, currentState: string);
534
- }
535
- /**
536
- * Error thrown during authentication process
537
- */
538
- declare class AuthenticationError extends T3nError {
539
- readonly authMethod?: string | undefined;
540
- constructor(message: string, authMethod?: string | undefined);
541
- }
542
- /**
543
- * Error thrown during handshake process
544
- */
545
- declare class HandshakeError extends T3nError {
546
- constructor(message: string);
547
- }
548
- /**
549
- * Error thrown during RPC communication.
550
- *
551
- * `message` is the human-readable error (preferring the server's
552
- * `error.data.detail` when present, with the request id appended in
553
- * `[<id>]` form so a UI that only surfaces `.message` still gives an
554
- * operator something to grep). The structured fields below preserve
555
- * the same info for callers that want to render or log them
556
- * separately — e.g. a toast that shows `detail` but surfaces
557
- * `requestId` in a "copy for support" affordance.
558
- */
559
- declare class RpcError extends T3nError {
560
- readonly rpcMethod?: string | undefined;
561
- readonly httpStatus?: number | undefined;
562
- /** Server-attached detail (JSON-RPC `error.data.detail`). User-facing kinds
563
- * carry the specific reason here; internal kinds omit it. */
564
- readonly detail?: string | undefined;
565
- /** Per-request correlation id (JSON-RPC `error.data.request_id`). */
566
- readonly requestId?: string | undefined;
567
- constructor(message: string, rpcMethod?: string | undefined, httpStatus?: number | undefined,
568
- /** Server-attached detail (JSON-RPC `error.data.detail`). User-facing kinds
569
- * carry the specific reason here; internal kinds omit it. */
570
- detail?: string | undefined,
571
- /** Per-request correlation id (JSON-RPC `error.data.request_id`). */
572
- requestId?: string | undefined);
573
- }
574
- /**
575
- * Error thrown when WASM operations fail
576
- */
577
- declare class WasmError extends T3nError {
578
- readonly operation?: string | undefined;
579
- readonly payload?: unknown | undefined;
580
- constructor(message: string, operation?: string | undefined, payload?: unknown | undefined);
581
- }
582
- /**
583
- * Decode WASM error message from comma-separated byte array format
584
- * WASM errors often come as "83,101,114,100,101..." which represents ASCII bytes
585
- *
586
- * @param errorMessage - The error message string that may contain comma-separated bytes
587
- * @returns Decoded error message if it was encoded, otherwise original message
588
- */
589
- declare function decodeWasmErrorMessage(errorMessage: string): string;
590
- /**
591
- * Extract and decode error from WASM ComponentError
592
- *
593
- * @param error - The error object from WASM
594
- * @returns Decoded error message
595
- */
596
- declare function extractWasmError(error: unknown): string;
597
-
598
793
  /**
599
794
  * Contract response decoding for {@link T3nClient.execute}.
600
795
  *
@@ -1030,6 +1225,116 @@ declare class T3nClient {
1030
1225
  * row exists yet), or if the response shape is unexpected.
1031
1226
  */
1032
1227
  kycStatus(providerId?: string): Promise<KycStatus>;
1228
+ /**
1229
+ * Dispatch a one-time code to the supplied contact via the host's
1230
+ * OTP provider. Backed by `tee:user/contracts::otp-request`.
1231
+ *
1232
+ * The contract persists the unverified contact in the channel's
1233
+ * pending slot (`pending_email` / `pending_phone`) and returns
1234
+ * `OtpRequestResult` with `status = "otp_pending"` (or `undefined`
1235
+ * when the node is configured with `skip_otp = true`). The next
1236
+ * step is {@link otpVerify} with the code the user typed.
1237
+ *
1238
+ * Behaviour notes:
1239
+ *
1240
+ * - Contact is a discriminated object: `emailChannel` or
1241
+ * `smsChannel` (mirrors Rust `OtpRequest`). The legacy
1242
+ * `keys.generic_api.otp_channel` body shadow is rejected by the
1243
+ * contract.
1244
+ * - SMS channel is gated on a verified email. Calling with
1245
+ * `channel = "sms"` against a DID that hasn't completed an
1246
+ * email roundtrip first throws.
1247
+ * - On a fresh DID's first call, `result.isNewProfile === true`.
1248
+ * Use this signal — not [[otpVerify]]'s response — for "did the
1249
+ * user just register" gating.
1250
+ *
1251
+ * @throws {RpcError} if the node rejects the action; the message
1252
+ * carries the contract-side detail (e.g. sequential-flow guard
1253
+ * when the email is missing).
1254
+ */
1255
+ otpRequest(input: OtpRequestInput): Promise<OtpRequestResult>;
1256
+ /**
1257
+ * Redeem a one-time code and bind the contact to the
1258
+ * authenticated DID. Backed by
1259
+ * `tee:user/contracts::otp-verify`.
1260
+ *
1261
+ * On success the contract promotes the pending contact back to
1262
+ * the canonical slot, writes the AUTH_MAP / DIDS_MAP /
1263
+ * USER_AUTHS_MAP authenticator entries, stamps
1264
+ * `verified_contacts.{email,phone}`, and mints the ambient
1265
+ * `t3n.personal.contact.1` ownership VC when both contacts are
1266
+ * now verified.
1267
+ *
1268
+ * If the contact is already owned by a different DID the
1269
+ * contract refuses to silently reparent the authenticator and
1270
+ * surfaces a `mergeSuggestion` instead — the caller resolves the
1271
+ * merge (typically via {@link mergeProfiles}) and re-attempts.
1272
+ *
1273
+ * On a wrong / expired code the contract returns the result with
1274
+ * `status = "otp_failed"` or `"otp_expired"` — no exception is
1275
+ * thrown, the error is surfaced as data so the UI can stay on
1276
+ * the verify screen and let the user retry.
1277
+ *
1278
+ * @throws {RpcError} if the node rejects the action outright
1279
+ * (network / decode / bad input shape). Branch on
1280
+ * `result.status` for retryable OTP failures.
1281
+ */
1282
+ otpVerify(input: OtpVerifyInput): Promise<OtpVerifyResult>;
1283
+ /**
1284
+ * Submit Level-1 user-input fields to the slim
1285
+ * `tee:user/contracts::user-upsert`. The contract merges the
1286
+ * supplied profile fields, validates them, mints
1287
+ * `t3n.user-input.kyc.1` once every Level-1 field is present, and
1288
+ * commits the write.
1289
+ *
1290
+ * **Pre-condition** (MAT-1374): the DID must already have a
1291
+ * verified email — either because {@link otpVerify} bound one or
1292
+ * because the session carries a proving authenticator (OIDC /
1293
+ * Email auth). Calls without proof are rejected with
1294
+ * {@link UserUpsertError} `kind = "EmailNotVerified"`. The
1295
+ * recommended UX is "request OTP -> verify OTP -> submit user
1296
+ * input" (or use {@link runOtpThenUserInput} which chains all
1297
+ * three).
1298
+ *
1299
+ * The KYC webhook orphan-attestation flow stays here: when
1300
+ * `requireExistingUser` is set, the contract identifies the user
1301
+ * by DID (vendorData) instead of email and the gate is bypassed.
1302
+ *
1303
+ * @throws {UserUpsertError} when the contract returns a typed
1304
+ * error (`email_not_verified`, `legacy_field`, `user_not_found`).
1305
+ * Branch on `err.kind`.
1306
+ * @throws {RpcError} for non-typed transport / decode failures.
1307
+ */
1308
+ submitUserInput(input: SubmitUserInputArgs): Promise<SubmitUserInputResult>;
1309
+ /**
1310
+ * Convenience helper: run the explicit OTP roundtrip and submit
1311
+ * the slim user-upsert in one call.
1312
+ *
1313
+ * The caller supplies a `getOtpCode(contact, channel)` callback
1314
+ * the SDK invokes between request and verify — this is where
1315
+ * a UI prompts the user to type the code that arrived on email
1316
+ * / SMS. Throw from the callback to abort the flow; the helper
1317
+ * does not retry on its own.
1318
+ *
1319
+ * Returns the slim `submitUserInput` result on success. Throws
1320
+ * {@link UserUpsertError} or `RpcError` on the same conditions
1321
+ * as the underlying wrappers.
1322
+ *
1323
+ * Optional, opt-in path — the recommended default is to call
1324
+ * {@link otpRequest}, {@link otpVerify}, and
1325
+ * {@link submitUserInput} explicitly so the application owns the
1326
+ * flow.
1327
+ */
1328
+ runOtpThenUserInput(args: {
1329
+ channel: OtpChannel;
1330
+ emailAddress?: string;
1331
+ phoneNumber?: string;
1332
+ profile: SubmitUserInputArgs["profile"];
1333
+ organisationDid?: string;
1334
+ attestations?: unknown;
1335
+ keys?: Record<string, unknown>;
1336
+ getOtpCode: (contact: string, channel: OtpChannel) => Promise<string> | string;
1337
+ }): Promise<SubmitUserInputResult>;
1033
1338
  /**
1034
1339
  * Poll `kyc-status` until a terminal status arrives or the
1035
1340
  * configured timeout elapses.
@@ -1423,5 +1728,5 @@ declare function clearKeyCache(): void;
1423
1728
  */
1424
1729
  declare function loadConfig(baseUrl?: string): SdkConfig;
1425
1730
 
1426
- export { AuthMethod, AuthenticationError, ContractResponseError, DEFAULT_KYC_POLL_CADENCE, HandshakeError, HttpTransport, KycStatusTimeoutError, LogLevel, MockTransport, NODE_URLS, RpcError, SessionStateError, SessionStatus, T3nClient, T3nError, TERMINAL_KYC_STATUSES, WasmError, bytesToString, clearKeyCache, createDefaultHandlers, createEthAuthInput, createLogger, createMlKemPublicKeyHandler, createOidcAuthInput, createRandomHandler, decodeWasmErrorMessage, eth_get_address, extractWasmError, fetchDkgAttestation, fetchMlKemPublicKey, generateRandomString, generateUUID, getEnvironment, getEnvironmentName, getGlobalLogLevel, getLogger, getNodeUrl, getScriptVersion, loadConfig, loadWasmComponent, metamask_get_address, metamask_sign, parseContractResponse, redactSecrets, redactSecretsFromJson, setEnvironment, setGlobalLogLevel, setNodeUrl, stringToBytes, validateConfig, verifyDkgAttestation, verifyTdxQuote };
1427
- export type { AuthInput, ClientAuth, ClientHandshake, ConfigValidationResult, ContractResponseSchema, Did, DkgAttestation, DkgVerifyResult, Environment, EthAuthInput, GuestToHostHandler, GuestToHostHandlers, HandshakeResult, JsonRpcRequest, JsonRpcResponse, KycPollCadence, KycPollOptions, KycStatus, KycStatusKind, Logger, OidcAuthInput, OidcCredentials, PeerQuoteResult, QuoteVerifyResult, SdkConfig, SessionCrypto, SessionId, T3nClientConfig, Transport, WasmComponent, WasmNextResult };
1731
+ export { AuthMethod, AuthenticationError, ContractResponseError, DEFAULT_KYC_POLL_CADENCE, HandshakeError, HttpTransport, KycStatusTimeoutError, LogLevel, MockTransport, NODE_URLS, RpcError, SessionStateError, SessionStatus, T3nClient, T3nError, TERMINAL_KYC_STATUSES, UserUpsertError, WasmError, bytesToString, clearKeyCache, createDefaultHandlers, createEthAuthInput, createLogger, createMlKemPublicKeyHandler, createOidcAuthInput, createRandomHandler, decodeWasmErrorMessage, eth_get_address, extractWasmError, fetchDkgAttestation, fetchMlKemPublicKey, generateRandomString, generateUUID, getEnvironment, getEnvironmentName, getGlobalLogLevel, getLogger, getNodeUrl, getScriptVersion, loadConfig, loadWasmComponent, metamask_get_address, metamask_sign, parseContractResponse, redactSecrets, redactSecretsFromJson, setEnvironment, setGlobalLogLevel, setNodeUrl, stringToBytes, validateConfig, verifyDkgAttestation, verifyTdxQuote };
1732
+ export type { AuthInput, ClientAuth, ClientHandshake, ConfigValidationResult, ContractResponseSchema, Did, DkgAttestation, DkgVerifyResult, Environment, EthAuthInput, GuestToHostHandler, GuestToHostHandlers, HandshakeResult, JsonRpcRequest, JsonRpcResponse, KycPollCadence, KycPollOptions, KycStatus, KycStatusKind, Logger, OidcAuthInput, OidcCredentials, OtpChannel, OtpMergeSuggestion, OtpRequestInput, OtpRequestResult, OtpVerifyInput, OtpVerifyResult, PeerQuoteResult, QuoteVerifyResult, SdkConfig, SessionCrypto, SessionId, SubmitUserInputArgs, SubmitUserInputResult, T3nClientConfig, Transport, UserInputProfile, UserUpsertErrorKind, WasmComponent, WasmNextResult };