@tesser-payments/sdk-ts 0.0.3 → 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,8 +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
- // POLYGON | POLYGON_AMOY | SOLANA
80
+ network: 'BASE_SEPOLIA',
76
81
  };
77
82
 
78
83
  const signed = await signer.signStep(step);
@@ -91,29 +96,54 @@ await fetch(
91
96
  ```
92
97
 
93
98
  `signStep` returns `{ signature, metadata }`. The `signature` field is what
94
- the Tesser API needs; `metadata` carries the underlying Turnkey activity body
95
- and stamp header value for debugging.
99
+ the Tesser API needs; `metadata` carries the signed body and stamp header value
100
+ for debugging.
96
101
 
97
102
  The `network` field is typed as `SupportedNetwork`, derived from the SDK's
98
- 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
99
104
  compile-time error. If you're pulling the value out of a webhook payload or
100
105
  env var, cast it at that boundary.
101
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
+
102
120
  ## Error handling
103
121
 
104
122
  The SDK mirrors Tesser's documented error envelope
105
123
  (<https://docs.tesser.xyz/overviews/errors>) rather than inventing a custom
106
- HTTP-status taxonomy. v0.0.1 only signs locally, so only the signing-side
107
- errors are exported:
124
+ HTTP-status taxonomy:
108
125
 
109
126
  ```
110
127
  TesserError (base; .cause set on all)
111
128
  ├── TesserConfigError bad input at construction or call site
112
- └── TesserSigningError local signing failure
113
- └── 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
114
136
  ```
115
137
 
116
- 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
117
147
  `instanceof` or the static `.is()` guard:
118
148
 
119
149
  ```typescript
@@ -123,7 +153,7 @@ try {
123
153
  await signer.signStep(step);
124
154
  } catch (err) {
125
155
  if (StampError.is(err)) {
126
- // Turnkey rejected the stamp — usually a key-format problem.
156
+ // The key could not sign — usually a key-format problem.
127
157
  }
128
158
  if (TesserError.is(err)) {
129
159
  console.error('Signing failed', { name: err.name, cause: err.cause });
@@ -132,29 +162,19 @@ try {
132
162
  }
133
163
  ```
134
164
 
135
- HTTP-side errors (`TesserAPIError`, `TesserConnectionError`,
136
- `TesserTimeoutError`) will return when the optional bundled client lands in a
137
- future version. Until then, model HTTP failures with whatever your `fetch`
138
- wrapper produces.
139
-
140
165
  ## Setup notes
141
166
 
142
- `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
143
168
  API-key auth lookups (66 hex chars, prefix `02` or `03`). Dashboards that
144
169
  display the 65-byte uncompressed form need to be compressed before use:
145
170
 
146
171
  ```sh
147
- bunx -p @turnkey/crypto node -e \
148
- 'const c = require("@turnkey/crypto"); \
149
- const u = process.argv[1].replace(/^0x/i,""); \
150
- const b = Buffer.from(u, "hex"); \
151
- 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"))' \
152
173
  04...your-uncompressed-key...
153
174
  ```
154
175
 
155
- If a key is registered on Turnkey in uncompressed form, Turnkey rejects
156
- auth with `PUBLIC_KEY_NOT_FOUND` even when the keypair is logically
157
- 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.
158
178
 
159
179
  ## Examples
160
180
 
@@ -172,9 +192,7 @@ copy [`.env.example`](./.env.example) and fill in the values first.
172
192
  ## Out of scope
173
193
 
174
194
  - **`TesserClient`** — the previous bundled HTTP client (`getAccountAddress`,
175
- retry/timeout/logger plumbing) is intentionally not in v0.0.1. We expect to
176
- re-add it later as an opt-in `LocalSignerOptions.client` field; that change
177
- is purely additive and will not break v0.0.1 call sites.
195
+ retry/timeout/logger plumbing) is intentionally not included.
178
196
  - **Webhook signature verification** — the header name and HMAC algorithm
179
197
  aren't currently documented in public Tesser docs.
180
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,15 +21777,40 @@ 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 = {
21490
21805
  BASE: "TRANSACTION_TYPE_ETHEREUM",
21491
21806
  BASE_SEPOLIA: "TRANSACTION_TYPE_ETHEREUM",
21492
21807
  ETHEREUM: "TRANSACTION_TYPE_ETHEREUM",
21808
+ ETHEREUM_SEPOLIA: "TRANSACTION_TYPE_ETHEREUM",
21493
21809
  POLYGON: "TRANSACTION_TYPE_ETHEREUM",
21494
21810
  POLYGON_AMOY: "TRANSACTION_TYPE_ETHEREUM",
21495
- SOLANA: "TRANSACTION_TYPE_SOLANA"
21811
+ SOLANA: "TRANSACTION_TYPE_SOLANA",
21812
+ TEMPO: "TRANSACTION_TYPE_TEMPO",
21813
+ TEMPO_MODERATO: "TRANSACTION_TYPE_TEMPO"
21496
21814
  };
21497
21815
  function networkToTurnkeyType(network) {
21498
21816
  const turnkeyType = NETWORK_TO_TURNKEY_TYPE[network];
@@ -21533,11 +21851,13 @@ async function signStep(signing, step) {
21533
21851
  class LocalSigner {
21534
21852
  signing;
21535
21853
  #privateKey;
21854
+ #api;
21536
21855
  constructor(options) {
21537
21856
  const s = options.signing;
21538
21857
  if (!s || !s.publicKey || !s.privateKey || !s.enclaveId) {
21539
21858
  throw new TesserConfigError("LocalSigner: signing.publicKey, signing.privateKey, and signing.enclaveId are required");
21540
21859
  }
21860
+ this.#api = createTesserApiConnection({ apiBaseUrl: options.apiBaseUrl, fetch: options.fetch });
21541
21861
  this.signing = { publicKey: s.publicKey, enclaveId: s.enclaveId };
21542
21862
  this.#privateKey = s.privateKey;
21543
21863
  }
@@ -21546,7 +21866,7 @@ class LocalSigner {
21546
21866
  publicKey: this.signing.publicKey,
21547
21867
  privateKey: this.#privateKey,
21548
21868
  enclaveId: this.signing.enclaveId
21549
- }, params);
21869
+ }, params, this.#api);
21550
21870
  }
21551
21871
  signStep(step) {
21552
21872
  return signStep({
@@ -21557,9 +21877,12 @@ class LocalSigner {
21557
21877
  }
21558
21878
  }
21559
21879
  export {
21880
+ TesserTimeoutError,
21560
21881
  TesserSigningError,
21561
21882
  TesserError,
21883
+ TesserConnectionError,
21562
21884
  TesserConfigError,
21885
+ TesserAPIError,
21563
21886
  StampError,
21564
21887
  LocalSigner
21565
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>;
@@ -2,9 +2,12 @@ declare const NETWORK_TO_TURNKEY_TYPE: {
2
2
  readonly BASE: "TRANSACTION_TYPE_ETHEREUM";
3
3
  readonly BASE_SEPOLIA: "TRANSACTION_TYPE_ETHEREUM";
4
4
  readonly ETHEREUM: "TRANSACTION_TYPE_ETHEREUM";
5
+ readonly ETHEREUM_SEPOLIA: "TRANSACTION_TYPE_ETHEREUM";
5
6
  readonly POLYGON: "TRANSACTION_TYPE_ETHEREUM";
6
7
  readonly POLYGON_AMOY: "TRANSACTION_TYPE_ETHEREUM";
7
8
  readonly SOLANA: "TRANSACTION_TYPE_SOLANA";
9
+ readonly TEMPO: "TRANSACTION_TYPE_TEMPO";
10
+ readonly TEMPO_MODERATO: "TRANSACTION_TYPE_TEMPO";
8
11
  };
9
12
  export type SupportedNetwork = keyof typeof NETWORK_TO_TURNKEY_TYPE;
10
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.3",
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
  ".": {
@@ -39,12 +39,19 @@
39
39
  "dependencies": {
40
40
  "@turnkey/api-key-stamper": "^0.6.5"
41
41
  },
42
+ "overrides": {
43
+ "ws": "^8.21.0",
44
+ "vite": "^6.4.5",
45
+ "postcss": "^8.5.18",
46
+ "source-map-js": "^1.2.2",
47
+ "zod": "^4.6.6"
48
+ },
42
49
  "devDependencies": {
43
50
  "@biomejs/biome": "^1.9",
44
51
  "@tesser-payments/types": "*",
45
52
  "@types/node": "^20",
46
53
  "typescript": "^5.4",
47
- "vitest": "^2"
54
+ "vitest": "^4.1.12"
48
55
  },
49
56
  "trustedDependencies": []
50
57
  }