@tesser-payments/sdk-ts 0.0.4 → 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/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # @tesser-payments/sdk-ts
2
2
 
3
- Signer-only TypeScript SDK for Tesser API: locally stamp `signCreateWallet` and
4
- `signStep` activities using Turnkey API keys. The SDK does not own HTTP or
5
- OAuth — Tesser API supplies the unsigned transaction; this library only stamps
6
- it. Bring your own client.
3
+ TypeScript SDK for the Tesser API: sign `signCreateWallet` and `signStep`
4
+ requests locally with your signing key. The SDK does not handle OAuth or submit
5
+ requests for you; bring your own client.
7
6
 
8
7
  ## Install
9
8
 
@@ -12,9 +11,7 @@ bun add @tesser-payments/sdk-ts @tesser-payments/types
12
11
  ```
13
12
 
14
13
  `@tesser-payments/types` is a peer dependency — install it alongside the SDK.
15
- `@turnkey/api-key-stamper` is a runtime dependency (used to locally stamp
16
- `signCreateWallet` and `signStep` activities) and is installed automatically;
17
- its Apache-2.0 license aligns with the SDK's. Apache 2.0 licensed.
14
+ Apache 2.0 licensed.
18
15
 
19
16
  ## Quick start
20
17
 
@@ -25,8 +22,9 @@ const signer = new LocalSigner({
25
22
  signing: {
26
23
  publicKey: process.env.SIGNING_PUBLIC_KEY!, // 33-byte compressed P-256
27
24
  privateKey: process.env.SIGNING_PRIVATE_KEY!,
28
- enclaveId: process.env.SIGNING_ENCLAVE_ID!, // Turnkey suborg ID
25
+ enclaveId: process.env.SIGNING_ENCLAVE_ID!,
29
26
  },
27
+ apiBaseUrl: 'https://api.tesser.xyz', // sandbox: https://api-sandbox.tesser.xyz
30
28
  });
31
29
 
32
30
  const { signature } = await signer.signCreateWallet({
@@ -52,6 +50,15 @@ const res = await fetch(`${API_BASE_URL}/v1/accounts/wallets`, {
52
50
  `signature` is `base64(JSON.stringify({ body, stamp }))` — drop it into the
53
51
  Tesser API request body as the documented `signature` field.
54
52
 
53
+ `LocalSigner` requires the Tesser API base URL (`https://api.tesser.xyz` or
54
+ `https://api-sandbox.tesser.xyz`); a trailing `/` or `/v1` is removed. Pass an
55
+ optional `fetch` implementation to configure a proxy.
56
+
57
+ `signCreateWallet` signs a wallet-creation request. For `stablecoin_ethereum`,
58
+ it first fetches the request to sign from the Tesser API and checks that it
59
+ matches your parameters. The private key never leaves your process. Tesser
60
+ currently supports managed wallets for `stablecoin_ethereum` only.
61
+
55
62
  **Private-key handling.** `signer.signing` exposes only `{ publicKey, enclaveId }`.
56
63
  The private key lives on the instance as an ECMA private field and is not
57
64
  reachable via property access, `console.log`, or `JSON.stringify`. The SDK still
@@ -59,8 +66,7 @@ uses it internally to stamp activities — you just can't pull it back out.
59
66
 
60
67
  ## `signStep`
61
68
 
62
- `signStep` stamps a Turnkey `ACTIVITY_TYPE_SIGN_TRANSACTION_V2` body for a
63
- single rebalance step. Pass the unsigned transaction Tesser returned, the
69
+ `signStep` signs a single rebalance step. Pass the unsigned transaction Tesser returned, the
64
70
  on-chain `signWith` address, and the network — that's all the SDK needs to
65
71
  produce a signature:
66
72
 
@@ -71,9 +77,7 @@ const step: StepForSigning = {
71
77
  unsignedTransaction: '0x02...', // hex-encoded raw tx Tesser returned
72
78
  signWith: '0xabc...', // account.crypto_wallet_address from
73
79
  // GET /v1/accounts/{from_account_id}
74
- network: 'BASE_SEPOLIA', // BASE | BASE_SEPOLIA | ETHEREUM |
75
- // ETHEREUM_SEPOLIA | POLYGON |
76
- // POLYGON_AMOY | SOLANA
80
+ network: 'BASE_SEPOLIA',
77
81
  };
78
82
 
79
83
  const signed = await signer.signStep(step);
@@ -92,29 +96,54 @@ await fetch(
92
96
  ```
93
97
 
94
98
  `signStep` returns `{ signature, metadata }`. The `signature` field is what
95
- the Tesser API needs; `metadata` carries the underlying Turnkey activity body
96
- and stamp header value for debugging.
99
+ the Tesser API needs; `metadata` carries the signed body and stamp header value
100
+ for debugging.
97
101
 
98
102
  The `network` field is typed as `SupportedNetwork`, derived from the SDK's
99
- network-to-Turnkey-type table — so a typo like `'BASE-SEPOLIA'` (hyphen) is a
103
+ supported-network table — so a typo like `'BASE-SEPOLIA'` (hyphen) is a
100
104
  compile-time error. If you're pulling the value out of a webhook payload or
101
105
  env var, cast it at that boundary.
102
106
 
107
+ Supported networks are `BASE`, `BASE_SEPOLIA`, `ETHEREUM`, `ETHEREUM_SEPOLIA`,
108
+ `POLYGON`, `POLYGON_AMOY`, `SOLANA`, `TEMPO`, and `TEMPO_MODERATO`.
109
+
110
+ ### Tempo transfers
111
+
112
+ 1. Pass the Tempo transaction Tesser provides (`0x76...`), your wallet
113
+ address, and `TEMPO` or `TEMPO_MODERATO` to `signStep`.
114
+ 2. The SDK authorizes the transaction with your API key, without changing
115
+ it. Submit the returned `signature` to the Tesser signing endpoint.
116
+ 3. Tesser completes the transfer, broadcasts it, and pays the network fee.
117
+
118
+ Unknown networks raise `TesserConfigError` before anything is signed.
119
+
103
120
  ## Error handling
104
121
 
105
122
  The SDK mirrors Tesser's documented error envelope
106
123
  (<https://docs.tesser.xyz/overviews/errors>) rather than inventing a custom
107
- HTTP-status taxonomy. v0.0.1 only signs locally, so only the signing-side
108
- errors are exported:
124
+ HTTP-status taxonomy:
109
125
 
110
126
  ```
111
127
  TesserError (base; .cause set on all)
112
128
  ├── TesserConfigError bad input at construction or call site
113
- └── TesserSigningError local signing failure
114
- └── StampError Turnkey ApiKeyStamper failed
129
+ ├── TesserAPIError Tesser answered with an error status or
130
+ │ an invalid response
131
+ ├── TesserConnectionError the request to Tesser failed at the network level
132
+ ├── TesserTimeoutError the request to Tesser timed out
133
+ └── TesserSigningError signing failure, or a fetched request that
134
+ │ does not match your parameters
135
+ └── StampError the signing key could not sign
115
136
  ```
116
137
 
117
- All three carry the original failure as `.cause` and can be discriminated via
138
+ `TesserAPIError`, `TesserConnectionError`, and `TesserTimeoutError` come from
139
+ the request `signCreateWallet` makes to Tesser for `stablecoin_ethereum`.
140
+ `TesserAPIError` carries `status`, `headers`, `requestId`, and the parsed
141
+ `errors[]` (with `errorCode` for the first one). `TesserConnectionError`,
142
+ `TesserTimeoutError`, and a `TesserAPIError` with status 429 or 5xx are safe to
143
+ retry; a `TesserAPIError` with any other 4xx status fails the same way on
144
+ retry.
145
+
146
+ Each error carries the original failure as `.cause` and can be discriminated via
118
147
  `instanceof` or the static `.is()` guard:
119
148
 
120
149
  ```typescript
@@ -124,7 +153,7 @@ try {
124
153
  await signer.signStep(step);
125
154
  } catch (err) {
126
155
  if (StampError.is(err)) {
127
- // Turnkey rejected the stamp — usually a key-format problem.
156
+ // The key could not sign — usually a key-format problem.
128
157
  }
129
158
  if (TesserError.is(err)) {
130
159
  console.error('Signing failed', { name: err.name, cause: err.cause });
@@ -133,29 +162,19 @@ try {
133
162
  }
134
163
  ```
135
164
 
136
- HTTP-side errors (`TesserAPIError`, `TesserConnectionError`,
137
- `TesserTimeoutError`) will return when the optional bundled client lands in a
138
- future version. Until then, model HTTP failures with whatever your `fetch`
139
- wrapper produces.
140
-
141
165
  ## Setup notes
142
166
 
143
- `signing.publicKey` must be the **33-byte compressed** form Turnkey uses for
167
+ `signing.publicKey` must be the **33-byte compressed** form Tesser uses for
144
168
  API-key auth lookups (66 hex chars, prefix `02` or `03`). Dashboards that
145
169
  display the 65-byte uncompressed form need to be compressed before use:
146
170
 
147
171
  ```sh
148
- bunx -p @turnkey/crypto node -e \
149
- 'const c = require("@turnkey/crypto"); \
150
- const u = process.argv[1].replace(/^0x/i,""); \
151
- const b = Buffer.from(u, "hex"); \
152
- console.log(Buffer.from(c.compressRawPublicKey(b)).toString("hex"))' \
172
+ node -e 'const u = process.argv[1].replace(/^0x/i, ""); console.log(require("node:crypto").ECDH.convertKey(u, "prime256v1", "hex", "hex", "compressed"))' \
153
173
  04...your-uncompressed-key...
154
174
  ```
155
175
 
156
- If a key is registered on Turnkey in uncompressed form, Turnkey rejects
157
- auth with `PUBLIC_KEY_NOT_FOUND` even when the keypair is logically
158
- correct — re-register the key in compressed form to fix.
176
+ If a key is registered in uncompressed form, signing is rejected even when the
177
+ keypair is logically correct — re-register the key in compressed form to fix.
159
178
 
160
179
  ## Examples
161
180
 
@@ -173,9 +192,7 @@ copy [`.env.example`](./.env.example) and fill in the values first.
173
192
  ## Out of scope
174
193
 
175
194
  - **`TesserClient`** — the previous bundled HTTP client (`getAccountAddress`,
176
- retry/timeout/logger plumbing) is intentionally not in v0.0.1. We expect to
177
- re-add it later as an opt-in `LocalSignerOptions.client` field; that change
178
- is purely additive and will not break v0.0.1 call sites.
195
+ retry/timeout/logger plumbing) is intentionally not included.
179
196
  - **Webhook signature verification** — the header name and HMAC algorithm
180
197
  aren't currently documented in public Tesser docs.
181
198
  - **Solana / Stellar `signCreateWallet`** — exported as `@experimental` types;
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { LocalSigner } from './signer.js';
2
2
  export type { PublicSigningInfo } from './signer.js';
3
3
  export type { CreateWalletParams, LocalSignerOptions, SignedResult, SignedResultMetadata, SignedStepResult, SignedStepResultMetadata, SigningConfig, StepForSigning, WalletType, } from './internal/types.js';
4
4
  export type { SupportedNetwork } from './signing/network-type.js';
5
- export { StampError, TesserConfigError, TesserError, TesserSigningError, } from './internal/errors.js';
5
+ export { StampError, TesserAPIError, TesserConfigError, TesserConnectionError, TesserError, TesserSigningError, TesserTimeoutError, } from './internal/errors.js';
6
6
  export type { TesserErrorDetail } from './internal/errors.js';
package/dist/index.js CHANGED
@@ -18273,6 +18273,47 @@ class TesserConfigError extends TesserError {
18273
18273
  return err instanceof TesserConfigError;
18274
18274
  }
18275
18275
  }
18276
+
18277
+ class TesserAPIError extends TesserError {
18278
+ name = "TesserAPIError";
18279
+ status;
18280
+ headers;
18281
+ requestId;
18282
+ errors;
18283
+ constructor(message, options) {
18284
+ super(message, { cause: options.cause });
18285
+ this.status = options.status;
18286
+ if (options.headers !== undefined)
18287
+ this.headers = options.headers;
18288
+ if (options.requestId !== undefined)
18289
+ this.requestId = options.requestId;
18290
+ this.errors = options.errors ?? [];
18291
+ }
18292
+ get errorCode() {
18293
+ return this.errors[0]?.errorCode;
18294
+ }
18295
+ hasCode(...codes) {
18296
+ return this.errors.some((e) => codes.includes(e.errorCode));
18297
+ }
18298
+ static is(err) {
18299
+ return err instanceof TesserAPIError;
18300
+ }
18301
+ }
18302
+
18303
+ class TesserConnectionError extends TesserError {
18304
+ name = "TesserConnectionError";
18305
+ static is(err) {
18306
+ return err instanceof TesserConnectionError;
18307
+ }
18308
+ }
18309
+
18310
+ class TesserTimeoutError extends TesserError {
18311
+ name = "TesserTimeoutError";
18312
+ static is(err) {
18313
+ return err instanceof TesserTimeoutError;
18314
+ }
18315
+ }
18316
+
18276
18317
  class TesserSigningError extends TesserError {
18277
18318
  name = "TesserSigningError";
18278
18319
  static is(err) {
@@ -19814,7 +19855,7 @@ var DNS = "dns";
19814
19855
  var DN = "dn";
19815
19856
  var EMAIL = "email";
19816
19857
  var IP = "ip";
19817
- var URL = "url";
19858
+ var URL2 = "url";
19818
19859
  var GUID = "guid";
19819
19860
  var UPN = "upn";
19820
19861
  var REGISTERED_ID = "id";
@@ -19870,7 +19911,7 @@ class GeneralName extends AsnData {
19870
19911
  });
19871
19912
  break;
19872
19913
  }
19873
- case URL:
19914
+ case URL2:
19874
19915
  name = new asn1X509__namespace.GeneralName({ uniformResourceIdentifier: args[1] });
19875
19916
  break;
19876
19917
  default:
@@ -19894,7 +19935,7 @@ class GeneralName extends AsnData {
19894
19935
  this.type = IP;
19895
19936
  this.value = asn.iPAddress;
19896
19937
  } else if (asn.uniformResourceIdentifier != null) {
19897
- this.type = URL;
19938
+ this.type = URL2;
19898
19939
  this.value = asn.uniformResourceIdentifier;
19899
19940
  } else if (asn.registeredID != null) {
19900
19941
  this.type = REGISTERED_ID;
@@ -19941,7 +19982,7 @@ class GeneralName extends AsnData {
19941
19982
  case IP:
19942
19983
  case REGISTERED_ID:
19943
19984
  case UPN:
19944
- case URL:
19985
+ case URL2:
19945
19986
  type = this.type.toUpperCase();
19946
19987
  break;
19947
19988
  case EMAIL:
@@ -21431,16 +21472,273 @@ async function stamp(keys, body) {
21431
21472
  }
21432
21473
  }
21433
21474
 
21475
+ // src/internal/version.ts
21476
+ var SDK_VERSION = "0.1.0";
21477
+
21478
+ // src/signing/unsigned-payload.ts
21479
+ var UNSIGNED_PAYLOAD_PATH = "/v1/accounts/wallets/unsigned-payload";
21480
+ var CREATE_WALLET_ACCOUNTS_ACTIVITY_TYPE = "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS";
21481
+ var ETHEREUM_ADDRESS_FORMAT = "ADDRESS_FORMAT_ETHEREUM";
21482
+ var UNSIGNED_PAYLOAD_TIMEOUT_MS = 1e4;
21483
+ var LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
21484
+ var MAX_FAILURE_CHAIN_DEPTH = 10;
21485
+ function createTesserApiConnection(options) {
21486
+ return {
21487
+ baseUrl: normalizeApiBaseUrl(options.apiBaseUrl),
21488
+ fetch: resolveFetchImplementation(options.fetch)
21489
+ };
21490
+ }
21491
+ function resolveFetchImplementation(injectedFetch) {
21492
+ if (injectedFetch !== undefined) {
21493
+ return injectedFetch;
21494
+ }
21495
+ return globalThis.fetch;
21496
+ }
21497
+ function normalizeApiBaseUrl(apiBaseUrl) {
21498
+ if (typeof apiBaseUrl !== "string") {
21499
+ throw invalidApiBaseUrlError();
21500
+ }
21501
+ const withoutTrailingSlash = removeSuffix(apiBaseUrl.trim(), "/");
21502
+ const normalized = removeSuffix(removeSuffix(withoutTrailingSlash, "/v1"), "/");
21503
+ if (!isHttpsOrLoopbackHttpUrl(normalized)) {
21504
+ throw invalidApiBaseUrlError();
21505
+ }
21506
+ return normalized;
21507
+ }
21508
+ function invalidApiBaseUrlError() {
21509
+ return new TesserConfigError("LocalSigner: apiBaseUrl must be an https URL, for example https://api.tesser.xyz");
21510
+ }
21511
+ function removeSuffix(value, suffix) {
21512
+ if (value.endsWith(suffix)) {
21513
+ return value.slice(0, value.length - suffix.length);
21514
+ }
21515
+ return value;
21516
+ }
21517
+ function isHttpsOrLoopbackHttpUrl(value) {
21518
+ const url = parseUrl(value);
21519
+ if (url === undefined || url.hostname === "") {
21520
+ return false;
21521
+ }
21522
+ if (url.protocol === "https:") {
21523
+ return true;
21524
+ }
21525
+ return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname.toLowerCase());
21526
+ }
21527
+ function parseUrl(value) {
21528
+ try {
21529
+ return new URL(value);
21530
+ } catch {
21531
+ return;
21532
+ }
21533
+ }
21534
+ async function fetchCheckedCreateWalletAccountsBody(options) {
21535
+ const { api, signing, params } = options;
21536
+ const query = buildUnsignedPayloadQuery(signing, params);
21537
+ const stamped = await stamp({ publicKey: signing.publicKey, privateKey: signing.privateKey }, buildCanonicalUnsignedPayloadMessage(query));
21538
+ const response = await sendUnsignedPayloadRequest(api, buildUnsignedPayloadUrl(api, query), stamped);
21539
+ const body = parseUnsignedPayloadBody(response);
21540
+ assertUnsignedPayloadMatchesRequest(body, signing, params);
21541
+ return body;
21542
+ }
21543
+ function buildUnsignedPayloadQuery(signing, params) {
21544
+ return {
21545
+ organizationId: signing.enclaveId,
21546
+ name: params.name,
21547
+ type: params.type,
21548
+ timestampMs: String(Date.now())
21549
+ };
21550
+ }
21551
+ function buildCanonicalUnsignedPayloadMessage(query) {
21552
+ return JSON.stringify({
21553
+ organizationId: query.organizationId,
21554
+ name: query.name,
21555
+ type: query.type,
21556
+ timestampMs: query.timestampMs
21557
+ });
21558
+ }
21559
+ function buildUnsignedPayloadUrl(api, query) {
21560
+ const queryEntries = [
21561
+ ["organizationId", query.organizationId],
21562
+ ["name", query.name],
21563
+ ["type", query.type],
21564
+ ["timestampMs", query.timestampMs]
21565
+ ];
21566
+ const queryString = queryEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
21567
+ return `${api.baseUrl}${UNSIGNED_PAYLOAD_PATH}?${queryString}`;
21568
+ }
21569
+ async function sendUnsignedPayloadRequest(api, url, stamped) {
21570
+ const fetchImplementation = api.fetch;
21571
+ try {
21572
+ const response = await fetchImplementation(url, {
21573
+ method: "GET",
21574
+ headers: {
21575
+ Accept: "application/json",
21576
+ [stamped.stampHeaderName]: stamped.stampHeaderValue,
21577
+ "User-Agent": `tesser-sdk-ts/${SDK_VERSION}`
21578
+ },
21579
+ signal: AbortSignal.timeout(UNSIGNED_PAYLOAD_TIMEOUT_MS)
21580
+ });
21581
+ const text = await response.text();
21582
+ return { status: response.status, headers: response.headers, text };
21583
+ } catch (cause) {
21584
+ throw toTransportError(cause);
21585
+ }
21586
+ }
21587
+ function toTransportError(cause) {
21588
+ if (isTimeoutFailure(cause)) {
21589
+ return new TesserTimeoutError(`Unsigned payload request timed out after ${UNSIGNED_PAYLOAD_TIMEOUT_MS} ms`, { cause });
21590
+ }
21591
+ return new TesserConnectionError(`Unsigned payload request failed: ${describeTransportFailure(cause)}`, { cause });
21592
+ }
21593
+ function isTimeoutFailure(cause) {
21594
+ return readErrorName(cause) === "TimeoutError";
21595
+ }
21596
+ function describeTransportFailure(cause) {
21597
+ const failureChain = collectFailureChain(cause);
21598
+ const messages = failureChain.map(readErrorMessage).filter(isNonBlank);
21599
+ if (messages.length > 0) {
21600
+ return [...new Set(messages)].join(": ");
21601
+ }
21602
+ const names2 = failureChain.map(readErrorName).filter(isNonBlank);
21603
+ return names2[names2.length - 1] ?? "Unknown transport failure";
21604
+ }
21605
+ function collectFailureChain(cause) {
21606
+ const failureChain = [];
21607
+ let current = cause;
21608
+ while (isObject(current) && failureChain.length < MAX_FAILURE_CHAIN_DEPTH) {
21609
+ if (failureChain.includes(current)) {
21610
+ break;
21611
+ }
21612
+ failureChain.push(current);
21613
+ current = current.cause;
21614
+ }
21615
+ return failureChain;
21616
+ }
21617
+ function readErrorMessage(failure) {
21618
+ return readStringField(failure, "message");
21619
+ }
21620
+ function readErrorName(failure) {
21621
+ return readStringField(failure, "name");
21622
+ }
21623
+ function readStringField(source, key) {
21624
+ if (!isObject(source)) {
21625
+ return;
21626
+ }
21627
+ const value = source[key];
21628
+ if (typeof value !== "string") {
21629
+ return;
21630
+ }
21631
+ return value;
21632
+ }
21633
+ function isNonBlank(value) {
21634
+ return value !== undefined && value.trim() !== "";
21635
+ }
21636
+ function isObject(value) {
21637
+ return typeof value === "object" && value !== null;
21638
+ }
21639
+ function readGatewayRequestId(headers) {
21640
+ return headers.get("zp-rid") ?? headers.get("x-request-id") ?? undefined;
21641
+ }
21642
+ function parseUnsignedPayloadBody(response) {
21643
+ if (response.status < 200 || response.status > 299) {
21644
+ throw buildUnsignedPayloadApiError(response);
21645
+ }
21646
+ const body = readResponseDataBody(response.text);
21647
+ if (body === undefined) {
21648
+ throw new TesserAPIError("Unsigned payload response is not valid: data.body is missing", buildApiErrorOptions(response, []));
21649
+ }
21650
+ return body;
21651
+ }
21652
+ function readResponseDataBody(text) {
21653
+ const envelope = parseJsonObject(text);
21654
+ if (envelope === undefined || !isObject(envelope.data)) {
21655
+ return;
21656
+ }
21657
+ return readStringField(envelope.data, "body");
21658
+ }
21659
+ function buildUnsignedPayloadApiError(response) {
21660
+ return new TesserAPIError(`Unsigned payload request returned HTTP ${response.status}: ${response.text}`, buildApiErrorOptions(response, parseErrorDetails(response.text)));
21661
+ }
21662
+ function buildApiErrorOptions(response, errors) {
21663
+ const requestId = readGatewayRequestId(response.headers);
21664
+ if (requestId === undefined) {
21665
+ return { status: response.status, headers: response.headers, errors };
21666
+ }
21667
+ return { status: response.status, headers: response.headers, requestId, errors };
21668
+ }
21669
+ function parseErrorDetails(text) {
21670
+ const envelope = parseJsonObject(text);
21671
+ if (envelope === undefined || !Array.isArray(envelope.errors)) {
21672
+ return [];
21673
+ }
21674
+ return envelope.errors.flatMap(toErrorDetail);
21675
+ }
21676
+ function toErrorDetail(entry) {
21677
+ const errorCode = readStringField(entry, "error_code");
21678
+ const errorMessage = readStringField(entry, "error_message");
21679
+ if (errorCode === undefined || errorMessage === undefined) {
21680
+ return [];
21681
+ }
21682
+ const uiMessage = readStringField(entry, "ui_message");
21683
+ if (uiMessage === undefined) {
21684
+ return [{ errorCode, errorMessage }];
21685
+ }
21686
+ return [{ errorCode, errorMessage, uiMessage }];
21687
+ }
21688
+ function parseJsonObject(text) {
21689
+ try {
21690
+ const parsed = JSON.parse(text);
21691
+ if (isObject(parsed) && !Array.isArray(parsed)) {
21692
+ return parsed;
21693
+ }
21694
+ return;
21695
+ } catch {
21696
+ return;
21697
+ }
21698
+ }
21699
+ function assertUnsignedPayloadMatchesRequest(body, signing, params) {
21700
+ const payload = parseJsonObject(body);
21701
+ if (payload === undefined) {
21702
+ throw rejectUnsignedPayload("body is not a JSON object");
21703
+ }
21704
+ if (readStringField(payload, "type") !== CREATE_WALLET_ACCOUNTS_ACTIVITY_TYPE) {
21705
+ throw rejectUnsignedPayload(`type is not ${CREATE_WALLET_ACCOUNTS_ACTIVITY_TYPE}`);
21706
+ }
21707
+ if (readStringField(payload, "organizationId") !== signing.enclaveId) {
21708
+ throw rejectUnsignedPayload("organizationId does not match the signing enclaveId");
21709
+ }
21710
+ const account = readSingleAccount(payload);
21711
+ if (readStringField(account, "name") !== params.name) {
21712
+ throw rejectUnsignedPayload("account name does not match the requested wallet name");
21713
+ }
21714
+ if (readStringField(account, "addressFormat") !== ETHEREUM_ADDRESS_FORMAT) {
21715
+ throw rejectUnsignedPayload(`account addressFormat is not ${ETHEREUM_ADDRESS_FORMAT}`);
21716
+ }
21717
+ }
21718
+ function readSingleAccount(payload) {
21719
+ const parameters = payload.parameters;
21720
+ if (!isObject(parameters)) {
21721
+ throw rejectUnsignedPayload("parameters is missing");
21722
+ }
21723
+ const accounts = parameters.accounts;
21724
+ if (!Array.isArray(accounts)) {
21725
+ throw rejectUnsignedPayload("parameters.accounts is missing");
21726
+ }
21727
+ if (accounts.length !== 1) {
21728
+ throw rejectUnsignedPayload(`expected exactly one account, got ${accounts.length}`);
21729
+ }
21730
+ const account = accounts[0];
21731
+ if (!isObject(account) || Array.isArray(account)) {
21732
+ throw rejectUnsignedPayload("account is not a JSON object");
21733
+ }
21734
+ return account;
21735
+ }
21736
+ function rejectUnsignedPayload(reason) {
21737
+ return new TesserSigningError(`Unsigned payload rejected: ${reason}`);
21738
+ }
21739
+
21434
21740
  // src/signing/create-wallet.ts
21435
21741
  var WALLET_TYPE_ACCOUNTS = {
21436
- stablecoin_ethereum: [
21437
- {
21438
- curve: "CURVE_SECP256K1",
21439
- pathFormat: "PATH_FORMAT_BIP32",
21440
- path: "m/44'/60'/0'/0/0",
21441
- addressFormat: "ADDRESS_FORMAT_ETHEREUM"
21442
- }
21443
- ],
21444
21742
  stablecoin_solana: [
21445
21743
  {
21446
21744
  curve: "CURVE_ED25519",
@@ -21458,21 +21756,16 @@ var WALLET_TYPE_ACCOUNTS = {
21458
21756
  }
21459
21757
  ]
21460
21758
  };
21461
- var VALID_WALLET_TYPES = Object.keys(WALLET_TYPE_ACCOUNTS);
21462
- async function signCreateWallet(signing, params) {
21759
+ var VALID_WALLET_TYPES = [
21760
+ "stablecoin_ethereum",
21761
+ "stablecoin_solana",
21762
+ "stablecoin_stellar"
21763
+ ];
21764
+ async function signCreateWallet(signing, params, api) {
21463
21765
  if (!VALID_WALLET_TYPES.includes(params.type)) {
21464
21766
  throw new TesserConfigError(`signCreateWallet: type '${String(params.type)}' is not a valid WalletType. ` + `Valid values: ${VALID_WALLET_TYPES.join(", ")}.`);
21465
21767
  }
21466
- const accounts = WALLET_TYPE_ACCOUNTS[params.type];
21467
- const body = JSON.stringify({
21468
- type: "ACTIVITY_TYPE_CREATE_WALLET",
21469
- timestampMs: String(Date.now()),
21470
- organizationId: signing.enclaveId,
21471
- parameters: {
21472
- walletName: params.name,
21473
- accounts
21474
- }
21475
- });
21768
+ const body = await resolveCreateWalletBody({ signing, params, api });
21476
21769
  const result = await stamp({ publicKey: signing.publicKey, privateKey: signing.privateKey }, body);
21477
21770
  const signature = Buffer.from(JSON.stringify({ body, stamp: result.stampHeaderValue })).toString("base64");
21478
21771
  return {
@@ -21484,6 +21777,28 @@ async function signCreateWallet(signing, params) {
21484
21777
  }
21485
21778
  };
21486
21779
  }
21780
+ async function resolveCreateWalletBody(options) {
21781
+ const { signing, params, api } = options;
21782
+ if (params.type === "stablecoin_ethereum") {
21783
+ return fetchCheckedCreateWalletAccountsBody({ api, signing, params });
21784
+ }
21785
+ return buildCreateWalletBody({
21786
+ enclaveId: signing.enclaveId,
21787
+ name: params.name,
21788
+ type: params.type
21789
+ });
21790
+ }
21791
+ function buildCreateWalletBody(options) {
21792
+ return JSON.stringify({
21793
+ type: "ACTIVITY_TYPE_CREATE_WALLET",
21794
+ timestampMs: String(Date.now()),
21795
+ organizationId: options.enclaveId,
21796
+ parameters: {
21797
+ walletName: options.name,
21798
+ accounts: WALLET_TYPE_ACCOUNTS[options.type]
21799
+ }
21800
+ });
21801
+ }
21487
21802
 
21488
21803
  // src/signing/network-type.ts
21489
21804
  var NETWORK_TO_TURNKEY_TYPE = {
@@ -21493,7 +21808,9 @@ var NETWORK_TO_TURNKEY_TYPE = {
21493
21808
  ETHEREUM_SEPOLIA: "TRANSACTION_TYPE_ETHEREUM",
21494
21809
  POLYGON: "TRANSACTION_TYPE_ETHEREUM",
21495
21810
  POLYGON_AMOY: "TRANSACTION_TYPE_ETHEREUM",
21496
- SOLANA: "TRANSACTION_TYPE_SOLANA"
21811
+ SOLANA: "TRANSACTION_TYPE_SOLANA",
21812
+ TEMPO: "TRANSACTION_TYPE_TEMPO",
21813
+ TEMPO_MODERATO: "TRANSACTION_TYPE_TEMPO"
21497
21814
  };
21498
21815
  function networkToTurnkeyType(network) {
21499
21816
  const turnkeyType = NETWORK_TO_TURNKEY_TYPE[network];
@@ -21534,11 +21851,13 @@ async function signStep(signing, step) {
21534
21851
  class LocalSigner {
21535
21852
  signing;
21536
21853
  #privateKey;
21854
+ #api;
21537
21855
  constructor(options) {
21538
21856
  const s = options.signing;
21539
21857
  if (!s || !s.publicKey || !s.privateKey || !s.enclaveId) {
21540
21858
  throw new TesserConfigError("LocalSigner: signing.publicKey, signing.privateKey, and signing.enclaveId are required");
21541
21859
  }
21860
+ this.#api = createTesserApiConnection({ apiBaseUrl: options.apiBaseUrl, fetch: options.fetch });
21542
21861
  this.signing = { publicKey: s.publicKey, enclaveId: s.enclaveId };
21543
21862
  this.#privateKey = s.privateKey;
21544
21863
  }
@@ -21547,7 +21866,7 @@ class LocalSigner {
21547
21866
  publicKey: this.signing.publicKey,
21548
21867
  privateKey: this.#privateKey,
21549
21868
  enclaveId: this.signing.enclaveId
21550
- }, params);
21869
+ }, params, this.#api);
21551
21870
  }
21552
21871
  signStep(step) {
21553
21872
  return signStep({
@@ -21558,9 +21877,12 @@ class LocalSigner {
21558
21877
  }
21559
21878
  }
21560
21879
  export {
21880
+ TesserTimeoutError,
21561
21881
  TesserSigningError,
21562
21882
  TesserError,
21883
+ TesserConnectionError,
21563
21884
  TesserConfigError,
21885
+ TesserAPIError,
21564
21886
  StampError,
21565
21887
  LocalSigner
21566
21888
  };
@@ -28,11 +28,11 @@ export interface TesserAPIErrorOptions {
28
28
  errors?: readonly TesserErrorDetail[];
29
29
  }
30
30
  /**
31
- * Thrown for any non-2xx HTTP response from Tesser. Carries the parsed
32
- * `errors[]` envelope so consumers can branch on Tesser's documented error
33
- * codes (e.g. `accounts-3005`, `idempotency-1001`) rather than HTTP status
34
- * alone. For responses that are not Tesser-shaped (e.g., a Cloudflare 502
35
- * page), `errors` is the empty array.
31
+ * Thrown when Tesser answers with an error status or an invalid response.
32
+ * Carries the parsed `errors[]` envelope so consumers can branch on Tesser's
33
+ * documented error codes (e.g. `accounts-3005`, `idempotency-1001`) rather
34
+ * than HTTP status alone. For responses that are not Tesser-shaped (e.g., a
35
+ * Cloudflare 502 page), `errors` is the empty array.
36
36
  */
37
37
  export declare class TesserAPIError extends TesserError {
38
38
  readonly name: string;
@@ -34,7 +34,7 @@ export interface StepForSigning {
34
34
  unsignedTransaction: string;
35
35
  /** On-chain address from GET /v1/accounts/{id} → crypto_wallet_address */
36
36
  signWith: string;
37
- /** Network identifier; one of the supported chains in `NETWORK_TO_TURNKEY_TYPE`. */
37
+ /** Network identifier; one of the supported networks. */
38
38
  network: SupportedNetwork;
39
39
  }
40
40
  export interface SignedStepResultMetadata {
@@ -49,4 +49,14 @@ export interface SignedStepResult {
49
49
  }
50
50
  export interface LocalSignerOptions {
51
51
  signing: SigningConfig;
52
+ /**
53
+ * Tesser API base URL: `https://api.tesser.xyz` in production,
54
+ * `https://api-sandbox.tesser.xyz` in sandbox. A trailing `/` or `/v1` is removed.
55
+ */
56
+ apiBaseUrl: string;
57
+ /**
58
+ * `fetch` implementation used for requests to Tesser. Inject one to configure a
59
+ * proxy. Defaults to the global `fetch`.
60
+ */
61
+ fetch?: typeof fetch;
52
62
  }
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "0.1.0";
package/dist/signer.d.ts CHANGED
@@ -11,9 +11,8 @@ export interface PublicSigningInfo {
11
11
  /**
12
12
  * Produces locally-signed activity payloads for Tesser API operations.
13
13
  *
14
- * v0.0.1 supports `signCreateWallet` and `signStep`. A future version will
15
- * add an optional `client` field to `LocalSignerOptions` for HTTP integration;
16
- * the addition is purely additive and does not break existing call sites.
14
+ * Supports `signCreateWallet` and `signStep`. Construction validates that the
15
+ * signing keys are non-blank and that `apiBaseUrl` is an https URL.
17
16
  *
18
17
  * Key handling: the private key is stored in an ECMA private field (`#privateKey`)
19
18
  * and is not reachable via `signer.signing.privateKey` or any other public
@@ -27,8 +26,25 @@ export declare class LocalSigner {
27
26
  #private;
28
27
  readonly signing: PublicSigningInfo;
29
28
  constructor(options: LocalSignerOptions);
30
- /** Stamps an `ACTIVITY_TYPE_CREATE_WALLET` Turnkey activity. */
29
+ /**
30
+ * Signs a wallet-creation request.
31
+ *
32
+ * For `stablecoin_ethereum` the SDK first fetches the request to sign from the
33
+ * Tesser API and checks that it matches your parameters. Other wallet types are
34
+ * signed locally without a request to Tesser. The private key never leaves the
35
+ * process.
36
+ *
37
+ * @throws {TesserConfigError} if the wallet type is not valid.
38
+ * @throws {TesserSigningError} if signing fails or the request fetched from
39
+ * Tesser does not match your parameters.
40
+ * @throws {TesserAPIError} if Tesser answers with an error status or an
41
+ * invalid response. Safe to retry for status 429 or 5xx; other 4xx statuses
42
+ * fail the same way on retry.
43
+ * @throws {TesserConnectionError} if the request to Tesser fails at the
44
+ * network level. Safe to retry.
45
+ * @throws {TesserTimeoutError} if the request to Tesser times out. Safe to retry.
46
+ */
31
47
  signCreateWallet(params: CreateWalletParams): Promise<SignedResult>;
32
- /** Stamps an `ACTIVITY_TYPE_SIGN_TRANSACTION_V2` Turnkey activity. */
48
+ /** Signs a single transfer step. The private key never leaves the process. */
33
49
  signStep(step: StepForSigning): Promise<SignedStepResult>;
34
50
  }
@@ -1,2 +1,3 @@
1
1
  import type { CreateWalletParams, SignedResult, SigningConfig } from '../internal/types.js';
2
- export declare function signCreateWallet(signing: SigningConfig, params: CreateWalletParams): Promise<SignedResult>;
2
+ import { type TesserApiConnection } from './unsigned-payload.js';
3
+ export declare function signCreateWallet(signing: SigningConfig, params: CreateWalletParams, api: TesserApiConnection): Promise<SignedResult>;
@@ -6,6 +6,8 @@ declare const NETWORK_TO_TURNKEY_TYPE: {
6
6
  readonly POLYGON: "TRANSACTION_TYPE_ETHEREUM";
7
7
  readonly POLYGON_AMOY: "TRANSACTION_TYPE_ETHEREUM";
8
8
  readonly SOLANA: "TRANSACTION_TYPE_SOLANA";
9
+ readonly TEMPO: "TRANSACTION_TYPE_TEMPO";
10
+ readonly TEMPO_MODERATO: "TRANSACTION_TYPE_TEMPO";
9
11
  };
10
12
  export type SupportedNetwork = keyof typeof NETWORK_TO_TURNKEY_TYPE;
11
13
  export declare function networkToTurnkeyType(network: string): string;
@@ -0,0 +1,15 @@
1
+ import type { CreateWalletParams, SigningConfig } from '../internal/types.js';
2
+ export interface TesserApiConnection {
3
+ baseUrl: string;
4
+ fetch: typeof fetch;
5
+ }
6
+ export declare function createTesserApiConnection(options: {
7
+ apiBaseUrl: unknown;
8
+ fetch: typeof fetch | undefined;
9
+ }): TesserApiConnection;
10
+ export declare function normalizeApiBaseUrl(apiBaseUrl: unknown): string;
11
+ export declare function fetchCheckedCreateWalletAccountsBody(options: {
12
+ api: TesserApiConnection;
13
+ signing: SigningConfig;
14
+ params: CreateWalletParams;
15
+ }): Promise<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tesser-payments/sdk-ts",
3
- "version": "0.0.4",
4
- "description": "Signer-only TypeScript SDK for Tesser API: locally stamp signCreateWallet and signStep activities. No HTTP, no OAuth — bring your own client.",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for Tesser API: sign signCreateWallet and signStep requests locally. No OAuth — bring your own client.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -41,14 +41,17 @@
41
41
  },
42
42
  "overrides": {
43
43
  "ws": "^8.21.0",
44
- "vite": "^6.4.3"
44
+ "vite": "^6.4.5",
45
+ "postcss": "^8.5.18",
46
+ "source-map-js": "^1.2.2",
47
+ "zod": "^4.6.6"
45
48
  },
46
49
  "devDependencies": {
47
50
  "@biomejs/biome": "^1.9",
48
51
  "@tesser-payments/types": "*",
49
52
  "@types/node": "^20",
50
53
  "typescript": "^5.4",
51
- "vitest": "^3.2.6"
54
+ "vitest": "^4.1.12"
52
55
  },
53
56
  "trustedDependencies": []
54
57
  }