@terminal3/t3n-sdk 1.3.1 → 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 +204 -0
- package/dist/index.d.ts +421 -82
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/dist/src/client/t3n-client.d.ts +145 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/user.d.ts +194 -0
- package/package.json +1 -1
|
@@ -8,12 +8,21 @@ 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
|
*/
|
|
14
15
|
export declare class T3nClient {
|
|
15
16
|
private readonly config;
|
|
16
17
|
private readonly transport;
|
|
18
|
+
/**
|
|
19
|
+
* Resolved node base URL. Snapshotted in the constructor so the
|
|
20
|
+
* typed contract wrappers (`kycStatus`, `getSelfEthAddress`, …)
|
|
21
|
+
* can call `getScriptVersion()` against the same host the
|
|
22
|
+
* transport talks to. Used only by the `script_version: "latest"`
|
|
23
|
+
* resolution path in {@link executeUserContract}.
|
|
24
|
+
*/
|
|
25
|
+
private readonly effectiveBaseUrl;
|
|
17
26
|
/**
|
|
18
27
|
* Server-minted session ID, set by {@link handshake} from the
|
|
19
28
|
* `Session-Id` response header (pentest M-1 / MAT-983). `null`
|
|
@@ -109,6 +118,32 @@ export declare class T3nClient {
|
|
|
109
118
|
* @throws {ContractResponseError} when the response is not valid JSON
|
|
110
119
|
*/
|
|
111
120
|
executeAndDecode<T = unknown>(payload: unknown, schema?: ContractResponseSchema<T>): Promise<T>;
|
|
121
|
+
/**
|
|
122
|
+
* Build the canonical `ExecuteActionRequest` shape the server
|
|
123
|
+
* expects in `node/primitives/src/action.rs::ExecuteActionRequest`
|
|
124
|
+
* (`script_name`, `script_version`, `function_name`, `input`,
|
|
125
|
+
* optional `pii_did`) and dispatch it through {@link execute}.
|
|
126
|
+
*
|
|
127
|
+
* Two pieces of glue live here that every typed user-contract
|
|
128
|
+
* wrapper would otherwise duplicate:
|
|
129
|
+
*
|
|
130
|
+
* 1. **Field naming.** The server deserialises strictly into
|
|
131
|
+
* `script_name` / `script_version` / `function_name` —
|
|
132
|
+
* sending `contract` / `version` / `function` produces
|
|
133
|
+
* `Invalid action request: missing field …` 400s. Centralising
|
|
134
|
+
* the names here means every wrapper agrees with the server.
|
|
135
|
+
* 2. **`"latest"` resolution.** `script_version` is `SemVer` on
|
|
136
|
+
* the server, so a literal `"latest"` cannot be parsed. We
|
|
137
|
+
* fetch the registered current version via
|
|
138
|
+
* `GET /api/contracts/current?name=…` (cached per script name
|
|
139
|
+
* in `getScriptVersion`) and forward the resolved
|
|
140
|
+
* `MAJOR.MINOR.PATCH` string.
|
|
141
|
+
*
|
|
142
|
+
* Wrappers that need PII delegation can extend this helper
|
|
143
|
+
* later — current call sites are all SelfOnly so `pii_did` stays
|
|
144
|
+
* implicit.
|
|
145
|
+
*/
|
|
146
|
+
private executeUserContract;
|
|
112
147
|
/**
|
|
113
148
|
* Return the authenticated user's Ethereum address from their
|
|
114
149
|
* T3N-hosted per-user wallet, as a 0x-prefixed lowercase hex string.
|
|
@@ -206,6 +241,116 @@ export declare class T3nClient {
|
|
|
206
241
|
* row exists yet), or if the response shape is unexpected.
|
|
207
242
|
*/
|
|
208
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>;
|
|
209
354
|
/**
|
|
210
355
|
* Poll `kyc-status` until a terminal status arrives or the
|
|
211
356
|
* configured timeout elapses.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ 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";
|
|
21
23
|
export { metamask_sign, metamask_get_address, eth_get_address, createDefaultHandlers, createMlKemPublicKeyHandler, createRandomHandler, } from "./client/handlers";
|
|
22
24
|
export type { WasmComponent, ClientHandshake, ClientAuth, SessionCrypto, WasmNextResult, } from "./wasm";
|
|
23
25
|
export { loadWasmComponent } from "./wasm";
|
|
@@ -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
|
+
}
|