tether-name 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Commit 451
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # tether-name
2
+
3
+ Official Node.js SDK for [tether.name](https://tether.name) — cryptographic identity verification for AI agents. Tether lets AI agents prove their identity using RSA-2048 signatures, enabling trusted agent-to-agent communication.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install tether-name
9
+ ```
10
+
11
+ Requires Node.js 18+ (uses native `fetch` and `crypto` modules).
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { TetherClient } from 'tether-name';
17
+
18
+ const client = new TetherClient({
19
+ credentialId: 'rgUOzbqar8z0Ag9RZH5I',
20
+ privateKeyPath: '/path/to/your/private-key.der'
21
+ });
22
+
23
+ // One-call verification
24
+ const result = await client.verify();
25
+ console.log(result.verified); // true
26
+ console.log(result.agentName); // "Jawnnybot"
27
+ console.log(result.verifyUrl); // "https://tether.name/check?challenge=..."
28
+ ```
29
+
30
+ ## Step-by-Step Usage
31
+
32
+ For more control over the verification process:
33
+
34
+ ```typescript
35
+ import { TetherClient } from 'tether-name';
36
+
37
+ const client = new TetherClient({
38
+ credentialId: 'rgUOzbqar8z0Ag9RZH5I',
39
+ privateKeyPem: `-----BEGIN RSA PRIVATE KEY-----
40
+ MIIEpAIBAAKCAQEA...
41
+ -----END RSA PRIVATE KEY-----`
42
+ });
43
+
44
+ try {
45
+ // 1. Request a challenge from Tether
46
+ const challenge = await client.requestChallenge();
47
+
48
+ // 2. Sign the challenge with your private key
49
+ const proof = client.sign(challenge);
50
+
51
+ // 3. Submit the proof for verification
52
+ const result = await client.submitProof(challenge, proof);
53
+
54
+ if (result.verified) {
55
+ console.log(`✅ Verified as ${result.agentName}`);
56
+ console.log(`📝 Public verification: ${result.verifyUrl}`);
57
+ } else {
58
+ console.log(`❌ Verification failed: ${result.error}`);
59
+ }
60
+ } catch (error) {
61
+ console.error('Verification error:', error.message);
62
+ }
63
+ ```
64
+
65
+ ## Configuration Options
66
+
67
+ ### Constructor Options
68
+
69
+ ```typescript
70
+ interface TetherClientConfig {
71
+ // Credential ID (required)
72
+ credentialId?: string; // Or use TETHER_CREDENTIAL_ID env var
73
+
74
+ // Private key (choose one)
75
+ privateKeyPath?: string; // Path to DER or PEM file
76
+ privateKeyPem?: string; // PEM string directly
77
+ privateKeyBuffer?: Buffer; // DER buffer directly
78
+
79
+ // Optional
80
+ baseUrl?: string; // API base URL (defaults to https://api.tether.name)
81
+ }
82
+ ```
83
+
84
+ ### Key Format Support
85
+
86
+ The SDK supports both DER and PEM private key formats:
87
+
88
+ ```typescript
89
+ // From file path (auto-detects format)
90
+ const client1 = new TetherClient({
91
+ credentialId: 'your-id',
92
+ privateKeyPath: '/path/to/key.der' // or .pem
93
+ });
94
+
95
+ // From PEM string
96
+ const client2 = new TetherClient({
97
+ credentialId: 'your-id',
98
+ privateKeyPem: '-----BEGIN RSA PRIVATE KEY-----\n...'
99
+ });
100
+
101
+ // From DER buffer
102
+ const derBuffer = fs.readFileSync('/path/to/key.der');
103
+ const client3 = new TetherClient({
104
+ credentialId: 'your-id',
105
+ privateKeyBuffer: derBuffer
106
+ });
107
+ ```
108
+
109
+ ## Environment Variables
110
+
111
+ Set these environment variables to avoid hardcoding credentials:
112
+
113
+ ```bash
114
+ export TETHER_CREDENTIAL_ID="your-credential-id"
115
+ export TETHER_PRIVATE_KEY_PATH="/path/to/your/private-key.der"
116
+ ```
117
+
118
+ Then initialize without parameters:
119
+
120
+ ```typescript
121
+ const client = new TetherClient({}); // Uses env vars
122
+ ```
123
+
124
+ ## API Reference
125
+
126
+ ### `TetherClient`
127
+
128
+ #### `constructor(config: TetherClientConfig)`
129
+
130
+ Creates a new Tether client instance.
131
+
132
+ #### `async verify(): Promise<VerificationResult>`
133
+
134
+ Performs complete verification in one call. Requests challenge, signs it, and submits proof.
135
+
136
+ **Throws:** `TetherVerificationError` if verification fails.
137
+
138
+ #### `async requestChallenge(): Promise<string>`
139
+
140
+ Requests a new challenge from the Tether API.
141
+
142
+ **Returns:** Challenge string to be signed.
143
+
144
+ #### `sign(challenge: string): string`
145
+
146
+ Signs a challenge using the configured private key.
147
+
148
+ **Returns:** URL-safe base64 signature (no padding).
149
+
150
+ #### `async submitProof(challenge: string, proof: string): Promise<VerificationResult>`
151
+
152
+ Submits signed proof to verify the challenge.
153
+
154
+ ### Types
155
+
156
+ ```typescript
157
+ interface VerificationResult {
158
+ verified: boolean; // Whether verification succeeded
159
+ agentName?: string; // Registered agent name
160
+ verifyUrl?: string; // Public verification URL
161
+ email?: string; // Registered email
162
+ registeredSince?: string; // ISO date of registration
163
+ error?: string; // Error message if failed
164
+ challenge?: string; // The verified challenge
165
+ }
166
+ ```
167
+
168
+ ### Errors
169
+
170
+ - `TetherError` - Base error class
171
+ - `TetherVerificationError` - Verification failed
172
+ - `TetherAPIError` - API request failed
173
+
174
+ ## Getting Your Credentials
175
+
176
+ 1. Visit [tether.name](https://tether.name)
177
+ 2. Register your agent and get a credential ID
178
+ 3. Generate an RSA-2048 private key:
179
+
180
+ ```bash
181
+ # Generate private key
182
+ openssl genrsa -out private-key.pem 2048
183
+
184
+ # Convert to DER format (optional)
185
+ openssl rsa -in private-key.pem -outform DER -out private-key.der
186
+ ```
187
+
188
+ ## Requirements
189
+
190
+ - Node.js 18+ (uses native `fetch`)
191
+ - RSA-2048 private key
192
+ - Zero runtime dependencies (uses only Node.js built-ins)
193
+
194
+ ## Security Notes
195
+
196
+ - Keep your private key secure and never commit it to version control
197
+ - Use environment variables or secure key management
198
+ - The SDK uses SHA256withRSA signatures with URL-safe base64 encoding
199
+ - All verification happens server-side at tether.name
200
+
201
+ ## License
202
+
203
+ MIT License - see [LICENSE](LICENSE) file for details.
204
+
205
+ ## Links
206
+
207
+ - 🌐 [Tether Website](https://tether.name)
208
+ - 📘 [Documentation](https://tether.name/docs)
209
+ - 🐛 [Issues](https://github.com/Commit451/tether-name-node/issues)
210
+ - 📦 [npm Package](https://www.npmjs.com/package/tether-name)
@@ -0,0 +1,133 @@
1
+ import { KeyObject } from 'crypto';
2
+
3
+ /**
4
+ * Configuration options for TetherClient
5
+ */
6
+ interface TetherClientConfig {
7
+ /** The credential ID for the agent */
8
+ credentialId?: string;
9
+ /** Path to the private key file (DER or PEM format) */
10
+ privateKeyPath?: string;
11
+ /** Private key as a string (PEM format) */
12
+ privateKeyPem?: string;
13
+ /** Private key as a Buffer (DER format) */
14
+ privateKeyBuffer?: Buffer;
15
+ /** Base URL for the Tether API (defaults to https://api.tether.name) */
16
+ baseUrl?: string;
17
+ }
18
+ /**
19
+ * Response from the challenge request endpoint
20
+ */
21
+ interface ChallengeResponse {
22
+ code: string;
23
+ }
24
+ /**
25
+ * Request payload for challenge verification
26
+ */
27
+ interface VerificationRequest {
28
+ challenge: string;
29
+ proof: string;
30
+ credentialId: string;
31
+ }
32
+ /**
33
+ * Response from the challenge verification endpoint
34
+ */
35
+ interface VerificationResponse {
36
+ valid: boolean;
37
+ verifyUrl?: string;
38
+ agentName?: string;
39
+ email?: string;
40
+ registeredSince?: string;
41
+ error?: string;
42
+ }
43
+ /**
44
+ * Result of a tether verification attempt
45
+ */
46
+ interface VerificationResult {
47
+ /** Whether the verification was successful */
48
+ verified: boolean;
49
+ /** The agent's registered name */
50
+ agentName?: string;
51
+ /** Public verification URL */
52
+ verifyUrl?: string;
53
+ /** The agent's registered email */
54
+ email?: string;
55
+ /** ISO date string of when the agent was registered */
56
+ registeredSince?: string;
57
+ /** Error message if verification failed */
58
+ error?: string;
59
+ /** The challenge that was verified */
60
+ challenge?: string;
61
+ }
62
+ /**
63
+ * Supported private key formats
64
+ */
65
+ type KeyFormat = 'pem' | 'der';
66
+
67
+ /**
68
+ * TetherClient - Official SDK for tether.name agent identity verification
69
+ */
70
+ declare class TetherClient {
71
+ private readonly credentialId;
72
+ private readonly privateKey;
73
+ private readonly baseUrl;
74
+ constructor(config: TetherClientConfig);
75
+ /**
76
+ * Request a challenge from the Tether API
77
+ */
78
+ requestChallenge(): Promise<string>;
79
+ /**
80
+ * Sign a challenge string
81
+ */
82
+ sign(challenge: string): string;
83
+ /**
84
+ * Submit proof for a challenge
85
+ */
86
+ submitProof(challenge: string, proof: string): Promise<VerificationResult>;
87
+ /**
88
+ * Perform complete verification in one call
89
+ */
90
+ verify(): Promise<VerificationResult>;
91
+ }
92
+
93
+ /**
94
+ * Base error class for all Tether-related errors
95
+ */
96
+ declare class TetherError extends Error {
97
+ readonly cause?: Error | undefined;
98
+ constructor(message: string, cause?: Error | undefined);
99
+ }
100
+ /**
101
+ * Error thrown when verification fails
102
+ */
103
+ declare class TetherVerificationError extends TetherError {
104
+ constructor(message: string, cause?: Error);
105
+ }
106
+ /**
107
+ * Error thrown when API requests fail
108
+ */
109
+ declare class TetherAPIError extends TetherError {
110
+ readonly status?: number | undefined;
111
+ readonly response?: string | undefined;
112
+ constructor(message: string, status?: number | undefined, response?: string | undefined, cause?: Error);
113
+ }
114
+
115
+ /**
116
+ * Loads a private key from various sources
117
+ */
118
+ declare function loadPrivateKey(options: {
119
+ keyPath?: string;
120
+ keyPem?: string;
121
+ keyBuffer?: Buffer;
122
+ }): KeyObject;
123
+ /**
124
+ * Signs a challenge string using RSA-SHA256
125
+ * Returns URL-safe base64 encoded signature (no padding)
126
+ */
127
+ declare function signChallenge(privateKey: KeyObject, challenge: string): string;
128
+ /**
129
+ * Utility function to detect key format from file extension or content
130
+ */
131
+ declare function detectKeyFormat(keyPath: string): KeyFormat;
132
+
133
+ export { type ChallengeResponse, type KeyFormat, TetherAPIError, TetherClient, type TetherClientConfig, TetherError, TetherVerificationError, type VerificationRequest, type VerificationResponse, type VerificationResult, detectKeyFormat, loadPrivateKey, signChallenge };
@@ -0,0 +1,133 @@
1
+ import { KeyObject } from 'crypto';
2
+
3
+ /**
4
+ * Configuration options for TetherClient
5
+ */
6
+ interface TetherClientConfig {
7
+ /** The credential ID for the agent */
8
+ credentialId?: string;
9
+ /** Path to the private key file (DER or PEM format) */
10
+ privateKeyPath?: string;
11
+ /** Private key as a string (PEM format) */
12
+ privateKeyPem?: string;
13
+ /** Private key as a Buffer (DER format) */
14
+ privateKeyBuffer?: Buffer;
15
+ /** Base URL for the Tether API (defaults to https://api.tether.name) */
16
+ baseUrl?: string;
17
+ }
18
+ /**
19
+ * Response from the challenge request endpoint
20
+ */
21
+ interface ChallengeResponse {
22
+ code: string;
23
+ }
24
+ /**
25
+ * Request payload for challenge verification
26
+ */
27
+ interface VerificationRequest {
28
+ challenge: string;
29
+ proof: string;
30
+ credentialId: string;
31
+ }
32
+ /**
33
+ * Response from the challenge verification endpoint
34
+ */
35
+ interface VerificationResponse {
36
+ valid: boolean;
37
+ verifyUrl?: string;
38
+ agentName?: string;
39
+ email?: string;
40
+ registeredSince?: string;
41
+ error?: string;
42
+ }
43
+ /**
44
+ * Result of a tether verification attempt
45
+ */
46
+ interface VerificationResult {
47
+ /** Whether the verification was successful */
48
+ verified: boolean;
49
+ /** The agent's registered name */
50
+ agentName?: string;
51
+ /** Public verification URL */
52
+ verifyUrl?: string;
53
+ /** The agent's registered email */
54
+ email?: string;
55
+ /** ISO date string of when the agent was registered */
56
+ registeredSince?: string;
57
+ /** Error message if verification failed */
58
+ error?: string;
59
+ /** The challenge that was verified */
60
+ challenge?: string;
61
+ }
62
+ /**
63
+ * Supported private key formats
64
+ */
65
+ type KeyFormat = 'pem' | 'der';
66
+
67
+ /**
68
+ * TetherClient - Official SDK for tether.name agent identity verification
69
+ */
70
+ declare class TetherClient {
71
+ private readonly credentialId;
72
+ private readonly privateKey;
73
+ private readonly baseUrl;
74
+ constructor(config: TetherClientConfig);
75
+ /**
76
+ * Request a challenge from the Tether API
77
+ */
78
+ requestChallenge(): Promise<string>;
79
+ /**
80
+ * Sign a challenge string
81
+ */
82
+ sign(challenge: string): string;
83
+ /**
84
+ * Submit proof for a challenge
85
+ */
86
+ submitProof(challenge: string, proof: string): Promise<VerificationResult>;
87
+ /**
88
+ * Perform complete verification in one call
89
+ */
90
+ verify(): Promise<VerificationResult>;
91
+ }
92
+
93
+ /**
94
+ * Base error class for all Tether-related errors
95
+ */
96
+ declare class TetherError extends Error {
97
+ readonly cause?: Error | undefined;
98
+ constructor(message: string, cause?: Error | undefined);
99
+ }
100
+ /**
101
+ * Error thrown when verification fails
102
+ */
103
+ declare class TetherVerificationError extends TetherError {
104
+ constructor(message: string, cause?: Error);
105
+ }
106
+ /**
107
+ * Error thrown when API requests fail
108
+ */
109
+ declare class TetherAPIError extends TetherError {
110
+ readonly status?: number | undefined;
111
+ readonly response?: string | undefined;
112
+ constructor(message: string, status?: number | undefined, response?: string | undefined, cause?: Error);
113
+ }
114
+
115
+ /**
116
+ * Loads a private key from various sources
117
+ */
118
+ declare function loadPrivateKey(options: {
119
+ keyPath?: string;
120
+ keyPem?: string;
121
+ keyBuffer?: Buffer;
122
+ }): KeyObject;
123
+ /**
124
+ * Signs a challenge string using RSA-SHA256
125
+ * Returns URL-safe base64 encoded signature (no padding)
126
+ */
127
+ declare function signChallenge(privateKey: KeyObject, challenge: string): string;
128
+ /**
129
+ * Utility function to detect key format from file extension or content
130
+ */
131
+ declare function detectKeyFormat(keyPath: string): KeyFormat;
132
+
133
+ export { type ChallengeResponse, type KeyFormat, TetherAPIError, TetherClient, type TetherClientConfig, TetherError, TetherVerificationError, type VerificationRequest, type VerificationResponse, type VerificationResult, detectKeyFormat, loadPrivateKey, signChallenge };
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ TetherAPIError: () => TetherAPIError,
24
+ TetherClient: () => TetherClient,
25
+ TetherError: () => TetherError,
26
+ TetherVerificationError: () => TetherVerificationError,
27
+ detectKeyFormat: () => detectKeyFormat,
28
+ loadPrivateKey: () => loadPrivateKey,
29
+ signChallenge: () => signChallenge
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/errors.ts
34
+ var TetherError = class _TetherError extends Error {
35
+ constructor(message, cause) {
36
+ super(message);
37
+ this.cause = cause;
38
+ this.name = "TetherError";
39
+ if (Error.captureStackTrace) {
40
+ Error.captureStackTrace(this, _TetherError);
41
+ }
42
+ }
43
+ };
44
+ var TetherVerificationError = class extends TetherError {
45
+ constructor(message, cause) {
46
+ super(message, cause);
47
+ this.name = "TetherVerificationError";
48
+ }
49
+ };
50
+ var TetherAPIError = class extends TetherError {
51
+ constructor(message, status, response, cause) {
52
+ super(message, cause);
53
+ this.status = status;
54
+ this.response = response;
55
+ this.name = "TetherAPIError";
56
+ }
57
+ };
58
+
59
+ // src/crypto.ts
60
+ var import_crypto = require("crypto");
61
+ var import_fs = require("fs");
62
+ function loadPrivateKey(options) {
63
+ const { keyPath, keyPem, keyBuffer } = options;
64
+ try {
65
+ if (keyPem) {
66
+ return (0, import_crypto.createPrivateKey)(keyPem);
67
+ }
68
+ if (keyBuffer) {
69
+ return (0, import_crypto.createPrivateKey)({
70
+ key: keyBuffer,
71
+ format: "der",
72
+ type: "pkcs1"
73
+ });
74
+ }
75
+ if (keyPath) {
76
+ const keyData = (0, import_fs.readFileSync)(keyPath);
77
+ if (keyPath.endsWith(".pem") || keyData.toString().includes("-----BEGIN")) {
78
+ return (0, import_crypto.createPrivateKey)(keyData);
79
+ } else {
80
+ return (0, import_crypto.createPrivateKey)({
81
+ key: keyData,
82
+ format: "der",
83
+ type: "pkcs1"
84
+ });
85
+ }
86
+ }
87
+ throw new TetherError("No private key provided");
88
+ } catch (error) {
89
+ if (error instanceof TetherError) {
90
+ throw error;
91
+ }
92
+ throw new TetherError(
93
+ `Failed to load private key: ${error instanceof Error ? error.message : String(error)}`,
94
+ error instanceof Error ? error : void 0
95
+ );
96
+ }
97
+ }
98
+ function signChallenge(privateKey, challenge) {
99
+ try {
100
+ const sign = (0, import_crypto.createSign)("SHA256");
101
+ sign.update(challenge);
102
+ sign.end();
103
+ const signature = sign.sign(privateKey);
104
+ return signature.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
105
+ } catch (error) {
106
+ throw new TetherError(
107
+ `Failed to sign challenge: ${error instanceof Error ? error.message : String(error)}`,
108
+ error instanceof Error ? error : void 0
109
+ );
110
+ }
111
+ }
112
+ function detectKeyFormat(keyPath) {
113
+ if (keyPath.endsWith(".pem")) {
114
+ return "pem";
115
+ }
116
+ if (keyPath.endsWith(".der")) {
117
+ return "der";
118
+ }
119
+ try {
120
+ const keyData = (0, import_fs.readFileSync)(keyPath, { encoding: "utf8", flag: "r" });
121
+ if (keyData.includes("-----BEGIN")) {
122
+ return "pem";
123
+ }
124
+ } catch {
125
+ }
126
+ return "der";
127
+ }
128
+
129
+ // src/client.ts
130
+ var TetherClient = class {
131
+ credentialId;
132
+ privateKey;
133
+ baseUrl;
134
+ constructor(config) {
135
+ this.credentialId = config.credentialId || process.env.TETHER_CREDENTIAL_ID || "";
136
+ if (!this.credentialId) {
137
+ throw new TetherError("Credential ID is required. Provide it in config or set TETHER_CREDENTIAL_ID environment variable.");
138
+ }
139
+ const keyPath = config.privateKeyPath || process.env.TETHER_PRIVATE_KEY_PATH;
140
+ this.privateKey = loadPrivateKey({
141
+ keyPath,
142
+ keyPem: config.privateKeyPem,
143
+ keyBuffer: config.privateKeyBuffer
144
+ });
145
+ this.baseUrl = config.baseUrl || "https://api.tether.name";
146
+ }
147
+ /**
148
+ * Request a challenge from the Tether API
149
+ */
150
+ async requestChallenge() {
151
+ try {
152
+ const response = await fetch(`${this.baseUrl}/challenge`, {
153
+ method: "POST",
154
+ headers: {
155
+ "Content-Type": "application/json"
156
+ }
157
+ });
158
+ if (!response.ok) {
159
+ const errorText = await response.text().catch(() => "Unknown error");
160
+ throw new TetherAPIError(
161
+ `Challenge request failed: ${response.status} ${response.statusText}`,
162
+ response.status,
163
+ errorText
164
+ );
165
+ }
166
+ const data = await response.json();
167
+ if (!data.code) {
168
+ throw new TetherAPIError("Invalid challenge response: missing code");
169
+ }
170
+ return data.code;
171
+ } catch (error) {
172
+ if (error instanceof TetherError) {
173
+ throw error;
174
+ }
175
+ throw new TetherAPIError(
176
+ `Failed to request challenge: ${error instanceof Error ? error.message : String(error)}`,
177
+ void 0,
178
+ void 0,
179
+ error instanceof Error ? error : void 0
180
+ );
181
+ }
182
+ }
183
+ /**
184
+ * Sign a challenge string
185
+ */
186
+ sign(challenge) {
187
+ return signChallenge(this.privateKey, challenge);
188
+ }
189
+ /**
190
+ * Submit proof for a challenge
191
+ */
192
+ async submitProof(challenge, proof) {
193
+ try {
194
+ const payload = {
195
+ challenge,
196
+ proof,
197
+ credentialId: this.credentialId
198
+ };
199
+ const response = await fetch(`${this.baseUrl}/challenge/verify`, {
200
+ method: "POST",
201
+ headers: {
202
+ "Content-Type": "application/json"
203
+ },
204
+ body: JSON.stringify(payload)
205
+ });
206
+ if (!response.ok) {
207
+ const errorText = await response.text().catch(() => "Unknown error");
208
+ throw new TetherAPIError(
209
+ `Verification failed: ${response.status} ${response.statusText}`,
210
+ response.status,
211
+ errorText
212
+ );
213
+ }
214
+ const data = await response.json();
215
+ return {
216
+ verified: data.valid,
217
+ agentName: data.agentName,
218
+ verifyUrl: data.verifyUrl,
219
+ email: data.email,
220
+ registeredSince: data.registeredSince,
221
+ error: data.error,
222
+ challenge
223
+ };
224
+ } catch (error) {
225
+ if (error instanceof TetherError) {
226
+ throw error;
227
+ }
228
+ throw new TetherAPIError(
229
+ `Failed to submit proof: ${error instanceof Error ? error.message : String(error)}`,
230
+ void 0,
231
+ void 0,
232
+ error instanceof Error ? error : void 0
233
+ );
234
+ }
235
+ }
236
+ /**
237
+ * Perform complete verification in one call
238
+ */
239
+ async verify() {
240
+ try {
241
+ const challenge = await this.requestChallenge();
242
+ const proof = this.sign(challenge);
243
+ const result = await this.submitProof(challenge, proof);
244
+ if (!result.verified) {
245
+ throw new TetherVerificationError(
246
+ result.error || "Verification failed for unknown reason"
247
+ );
248
+ }
249
+ return result;
250
+ } catch (error) {
251
+ if (error instanceof TetherError) {
252
+ throw error;
253
+ }
254
+ throw new TetherVerificationError(
255
+ `Verification failed: ${error instanceof Error ? error.message : String(error)}`,
256
+ error instanceof Error ? error : void 0
257
+ );
258
+ }
259
+ }
260
+ };
261
+ // Annotate the CommonJS export names for ESM import in node:
262
+ 0 && (module.exports = {
263
+ TetherAPIError,
264
+ TetherClient,
265
+ TetherError,
266
+ TetherVerificationError,
267
+ detectKeyFormat,
268
+ loadPrivateKey,
269
+ signChallenge
270
+ });
271
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/crypto.ts","../src/client.ts"],"sourcesContent":["/**\n * Tether Name SDK - Official Node.js library for tether.name agent identity verification\n * \n * @example\n * ```typescript\n * import { TetherClient } from 'tether-name';\n * \n * const client = new TetherClient({\n * credentialId: 'your-credential-id',\n * privateKeyPath: '/path/to/key.der'\n * });\n * \n * const result = await client.verify();\n * console.log(result.verified, result.agentName);\n * ```\n */\n\n// Main exports\nexport { TetherClient } from './client.js';\n\n// Types\nexport type {\n TetherClientConfig,\n ChallengeResponse,\n VerificationRequest,\n VerificationResponse,\n VerificationResult,\n KeyFormat\n} from './types.js';\n\n// Errors\nexport {\n TetherError,\n TetherAPIError,\n TetherVerificationError\n} from './errors.js';\n\n// Crypto utilities (for advanced use cases)\nexport {\n loadPrivateKey,\n signChallenge,\n detectKeyFormat\n} from './crypto.js';","/**\n * Base error class for all Tether-related errors\n */\nexport class TetherError extends Error {\n constructor(message: string, public readonly cause?: Error) {\n super(message);\n this.name = 'TetherError';\n \n // Maintain proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TetherError);\n }\n }\n}\n\n/**\n * Error thrown when verification fails\n */\nexport class TetherVerificationError extends TetherError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'TetherVerificationError';\n }\n}\n\n/**\n * Error thrown when API requests fail\n */\nexport class TetherAPIError extends TetherError {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly response?: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'TetherAPIError';\n }\n}","import { createSign, createPrivateKey, KeyObject } from 'crypto';\nimport { readFileSync } from 'fs';\nimport { TetherError } from './errors.js';\nimport type { KeyFormat } from './types.js';\n\n/**\n * Loads a private key from various sources\n */\nexport function loadPrivateKey(options: {\n keyPath?: string;\n keyPem?: string;\n keyBuffer?: Buffer;\n}): KeyObject {\n const { keyPath, keyPem, keyBuffer } = options;\n\n try {\n if (keyPem) {\n // PEM string provided directly\n return createPrivateKey(keyPem);\n }\n \n if (keyBuffer) {\n // DER buffer provided directly\n return createPrivateKey({\n key: keyBuffer,\n format: 'der',\n type: 'pkcs1'\n });\n }\n \n if (keyPath) {\n // Read from file - detect format by extension or content\n const keyData = readFileSync(keyPath);\n \n // Try to detect format\n if (keyPath.endsWith('.pem') || keyData.toString().includes('-----BEGIN')) {\n // PEM format\n return createPrivateKey(keyData);\n } else {\n // Assume DER format\n return createPrivateKey({\n key: keyData,\n format: 'der',\n type: 'pkcs1'\n });\n }\n }\n \n throw new TetherError('No private key provided');\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherError(\n `Failed to load private key: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Signs a challenge string using RSA-SHA256\n * Returns URL-safe base64 encoded signature (no padding)\n */\nexport function signChallenge(privateKey: KeyObject, challenge: string): string {\n try {\n const sign = createSign('SHA256');\n sign.update(challenge);\n sign.end();\n \n const signature = sign.sign(privateKey);\n \n // Convert to URL-safe base64 without padding\n return signature\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '');\n } catch (error) {\n throw new TetherError(\n `Failed to sign challenge: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Utility function to detect key format from file extension or content\n */\nexport function detectKeyFormat(keyPath: string): KeyFormat {\n if (keyPath.endsWith('.pem')) {\n return 'pem';\n }\n if (keyPath.endsWith('.der')) {\n return 'der';\n }\n \n // Try to read a small portion to detect format\n try {\n const keyData = readFileSync(keyPath, { encoding: 'utf8', flag: 'r' });\n if (keyData.includes('-----BEGIN')) {\n return 'pem';\n }\n } catch {\n // If we can't read as text, it's probably DER\n }\n \n return 'der';\n}","import { KeyObject } from 'crypto';\nimport { TetherError, TetherAPIError, TetherVerificationError } from './errors.js';\nimport { loadPrivateKey, signChallenge } from './crypto.js';\nimport type {\n TetherClientConfig,\n ChallengeResponse,\n VerificationRequest,\n VerificationResponse,\n VerificationResult\n} from './types.js';\n\n/**\n * TetherClient - Official SDK for tether.name agent identity verification\n */\nexport class TetherClient {\n private readonly credentialId: string;\n private readonly privateKey: KeyObject;\n private readonly baseUrl: string;\n\n constructor(config: TetherClientConfig) {\n // Get credential ID from config or environment\n this.credentialId = config.credentialId || process.env.TETHER_CREDENTIAL_ID || '';\n if (!this.credentialId) {\n throw new TetherError('Credential ID is required. Provide it in config or set TETHER_CREDENTIAL_ID environment variable.');\n }\n\n // Load private key\n const keyPath = config.privateKeyPath || process.env.TETHER_PRIVATE_KEY_PATH;\n this.privateKey = loadPrivateKey({\n keyPath,\n keyPem: config.privateKeyPem,\n keyBuffer: config.privateKeyBuffer\n });\n\n // Set base URL\n this.baseUrl = config.baseUrl || 'https://api.tether.name';\n }\n\n /**\n * Request a challenge from the Tether API\n */\n async requestChallenge(): Promise<string> {\n try {\n const response = await fetch(`${this.baseUrl}/challenge`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new TetherAPIError(\n `Challenge request failed: ${response.status} ${response.statusText}`,\n response.status,\n errorText\n );\n }\n\n const data = await response.json() as ChallengeResponse;\n \n if (!data.code) {\n throw new TetherAPIError('Invalid challenge response: missing code');\n }\n\n return data.code;\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherAPIError(\n `Failed to request challenge: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Sign a challenge string\n */\n sign(challenge: string): string {\n return signChallenge(this.privateKey, challenge);\n }\n\n /**\n * Submit proof for a challenge\n */\n async submitProof(challenge: string, proof: string): Promise<VerificationResult> {\n try {\n const payload: VerificationRequest = {\n challenge,\n proof,\n credentialId: this.credentialId\n };\n\n const response = await fetch(`${this.baseUrl}/challenge/verify`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new TetherAPIError(\n `Verification failed: ${response.status} ${response.statusText}`,\n response.status,\n errorText\n );\n }\n\n const data = await response.json() as VerificationResponse;\n\n // Convert API response to our result format\n return {\n verified: data.valid,\n agentName: data.agentName,\n verifyUrl: data.verifyUrl,\n email: data.email,\n registeredSince: data.registeredSince,\n error: data.error,\n challenge\n };\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherAPIError(\n `Failed to submit proof: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Perform complete verification in one call\n */\n async verify(): Promise<VerificationResult> {\n try {\n const challenge = await this.requestChallenge();\n const proof = this.sign(challenge);\n const result = await this.submitProof(challenge, proof);\n\n if (!result.verified) {\n throw new TetherVerificationError(\n result.error || 'Verification failed for unknown reason'\n );\n }\n\n return result;\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherVerificationError(\n `Verification failed: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n }\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGO,IAAM,cAAN,MAAM,qBAAoB,MAAM;AAAA,EACrC,YAAY,SAAiC,OAAe;AAC1D,UAAM,OAAO;AAD8B;AAE3C,SAAK,OAAO;AAGZ,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,YAAW;AAAA,IAC3C;AAAA,EACF;AACF;AAKO,IAAM,0BAAN,cAAsC,YAAY;AAAA,EACvD,YAAY,SAAiB,OAAe;AAC1C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,iBAAN,cAA6B,YAAY;AAAA,EAC9C,YACE,SACgB,QACA,UAChB,OACA;AACA,UAAM,SAAS,KAAK;AAJJ;AACA;AAIhB,SAAK,OAAO;AAAA,EACd;AACF;;;ACtCA,oBAAwD;AACxD,gBAA6B;AAOtB,SAAS,eAAe,SAIjB;AACZ,QAAM,EAAE,SAAS,QAAQ,UAAU,IAAI;AAEvC,MAAI;AACF,QAAI,QAAQ;AAEV,iBAAO,gCAAiB,MAAM;AAAA,IAChC;AAEA,QAAI,WAAW;AAEb,iBAAO,gCAAiB;AAAA,QACtB,KAAK;AAAA,QACL,QAAQ;AAAA,QACR,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,QAAI,SAAS;AAEX,YAAM,cAAU,wBAAa,OAAO;AAGpC,UAAI,QAAQ,SAAS,MAAM,KAAK,QAAQ,SAAS,EAAE,SAAS,YAAY,GAAG;AAEzE,mBAAO,gCAAiB,OAAO;AAAA,MACjC,OAAO;AAEL,mBAAO,gCAAiB;AAAA,UACtB,KAAK;AAAA,UACL,QAAQ;AAAA,UACR,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,IAAI,YAAY,yBAAyB;AAAA,EACjD,SAAS,OAAO;AACd,QAAI,iBAAiB,aAAa;AAChC,YAAM;AAAA,IACR;AACA,UAAM,IAAI;AAAA,MACR,+BAA+B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,MACrF,iBAAiB,QAAQ,QAAQ;AAAA,IACnC;AAAA,EACF;AACF;AAMO,SAAS,cAAc,YAAuB,WAA2B;AAC9E,MAAI;AACF,UAAM,WAAO,0BAAW,QAAQ;AAChC,SAAK,OAAO,SAAS;AACrB,SAAK,IAAI;AAET,UAAM,YAAY,KAAK,KAAK,UAAU;AAGtC,WAAO,UACJ,SAAS,QAAQ,EACjB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,EAAE;AAAA,EACrB,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,6BAA6B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,MACnF,iBAAiB,QAAQ,QAAQ;AAAA,IACnC;AAAA,EACF;AACF;AAKO,SAAS,gBAAgB,SAA4B;AAC1D,MAAI,QAAQ,SAAS,MAAM,GAAG;AAC5B,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS,MAAM,GAAG;AAC5B,WAAO;AAAA,EACT;AAGA,MAAI;AACF,UAAM,cAAU,wBAAa,SAAS,EAAE,UAAU,QAAQ,MAAM,IAAI,CAAC;AACrE,QAAI,QAAQ,SAAS,YAAY,GAAG;AAClC,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;;;AC9FO,IAAM,eAAN,MAAmB;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,QAA4B;AAEtC,SAAK,eAAe,OAAO,gBAAgB,QAAQ,IAAI,wBAAwB;AAC/E,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI,YAAY,mGAAmG;AAAA,IAC3H;AAGA,UAAM,UAAU,OAAO,kBAAkB,QAAQ,IAAI;AACrD,SAAK,aAAa,eAAe;AAAA,MAC/B;AAAA,MACA,QAAQ,OAAO;AAAA,MACf,WAAW,OAAO;AAAA,IACpB,CAAC;AAGD,SAAK,UAAU,OAAO,WAAW;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,mBAAoC;AACxC,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,cAAc;AAAA,QACxD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,MACF,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,eAAe;AACnE,cAAM,IAAI;AAAA,UACR,6BAA6B,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,UACnE,SAAS;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,UAAI,CAAC,KAAK,MAAM;AACd,cAAM,IAAI,eAAe,0CAA0C;AAAA,MACrE;AAEA,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AACd,UAAI,iBAAiB,aAAa;AAChC,cAAM;AAAA,MACR;AACA,YAAM,IAAI;AAAA,QACR,gCAAgC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QACtF;AAAA,QACA;AAAA,QACA,iBAAiB,QAAQ,QAAQ;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK,WAA2B;AAC9B,WAAO,cAAc,KAAK,YAAY,SAAS;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,WAAmB,OAA4C;AAC/E,QAAI;AACF,YAAM,UAA+B;AAAA,QACnC;AAAA,QACA;AAAA,QACA,cAAc,KAAK;AAAA,MACrB;AAEA,YAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,qBAAqB;AAAA,QAC/D,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,eAAe;AACnE,cAAM,IAAI;AAAA,UACR,wBAAwB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,UAC9D,SAAS;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,aAAO;AAAA,QACL,UAAU,KAAK;AAAA,QACf,WAAW,KAAK;AAAA,QAChB,WAAW,KAAK;AAAA,QAChB,OAAO,KAAK;AAAA,QACZ,iBAAiB,KAAK;AAAA,QACtB,OAAO,KAAK;AAAA,QACZ;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,aAAa;AAChC,cAAM;AAAA,MACR;AACA,YAAM,IAAI;AAAA,QACR,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QACjF;AAAA,QACA;AAAA,QACA,iBAAiB,QAAQ,QAAQ;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAsC;AAC1C,QAAI;AACF,YAAM,YAAY,MAAM,KAAK,iBAAiB;AAC9C,YAAM,QAAQ,KAAK,KAAK,SAAS;AACjC,YAAM,SAAS,MAAM,KAAK,YAAY,WAAW,KAAK;AAEtD,UAAI,CAAC,OAAO,UAAU;AACpB,cAAM,IAAI;AAAA,UACR,OAAO,SAAS;AAAA,QAClB;AAAA,MACF;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,iBAAiB,aAAa;AAChC,cAAM;AAAA,MACR;AACA,YAAM,IAAI;AAAA,QACR,wBAAwB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC9E,iBAAiB,QAAQ,QAAQ;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,238 @@
1
+ // src/errors.ts
2
+ var TetherError = class _TetherError extends Error {
3
+ constructor(message, cause) {
4
+ super(message);
5
+ this.cause = cause;
6
+ this.name = "TetherError";
7
+ if (Error.captureStackTrace) {
8
+ Error.captureStackTrace(this, _TetherError);
9
+ }
10
+ }
11
+ };
12
+ var TetherVerificationError = class extends TetherError {
13
+ constructor(message, cause) {
14
+ super(message, cause);
15
+ this.name = "TetherVerificationError";
16
+ }
17
+ };
18
+ var TetherAPIError = class extends TetherError {
19
+ constructor(message, status, response, cause) {
20
+ super(message, cause);
21
+ this.status = status;
22
+ this.response = response;
23
+ this.name = "TetherAPIError";
24
+ }
25
+ };
26
+
27
+ // src/crypto.ts
28
+ import { createSign, createPrivateKey } from "crypto";
29
+ import { readFileSync } from "fs";
30
+ function loadPrivateKey(options) {
31
+ const { keyPath, keyPem, keyBuffer } = options;
32
+ try {
33
+ if (keyPem) {
34
+ return createPrivateKey(keyPem);
35
+ }
36
+ if (keyBuffer) {
37
+ return createPrivateKey({
38
+ key: keyBuffer,
39
+ format: "der",
40
+ type: "pkcs1"
41
+ });
42
+ }
43
+ if (keyPath) {
44
+ const keyData = readFileSync(keyPath);
45
+ if (keyPath.endsWith(".pem") || keyData.toString().includes("-----BEGIN")) {
46
+ return createPrivateKey(keyData);
47
+ } else {
48
+ return createPrivateKey({
49
+ key: keyData,
50
+ format: "der",
51
+ type: "pkcs1"
52
+ });
53
+ }
54
+ }
55
+ throw new TetherError("No private key provided");
56
+ } catch (error) {
57
+ if (error instanceof TetherError) {
58
+ throw error;
59
+ }
60
+ throw new TetherError(
61
+ `Failed to load private key: ${error instanceof Error ? error.message : String(error)}`,
62
+ error instanceof Error ? error : void 0
63
+ );
64
+ }
65
+ }
66
+ function signChallenge(privateKey, challenge) {
67
+ try {
68
+ const sign = createSign("SHA256");
69
+ sign.update(challenge);
70
+ sign.end();
71
+ const signature = sign.sign(privateKey);
72
+ return signature.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
73
+ } catch (error) {
74
+ throw new TetherError(
75
+ `Failed to sign challenge: ${error instanceof Error ? error.message : String(error)}`,
76
+ error instanceof Error ? error : void 0
77
+ );
78
+ }
79
+ }
80
+ function detectKeyFormat(keyPath) {
81
+ if (keyPath.endsWith(".pem")) {
82
+ return "pem";
83
+ }
84
+ if (keyPath.endsWith(".der")) {
85
+ return "der";
86
+ }
87
+ try {
88
+ const keyData = readFileSync(keyPath, { encoding: "utf8", flag: "r" });
89
+ if (keyData.includes("-----BEGIN")) {
90
+ return "pem";
91
+ }
92
+ } catch {
93
+ }
94
+ return "der";
95
+ }
96
+
97
+ // src/client.ts
98
+ var TetherClient = class {
99
+ credentialId;
100
+ privateKey;
101
+ baseUrl;
102
+ constructor(config) {
103
+ this.credentialId = config.credentialId || process.env.TETHER_CREDENTIAL_ID || "";
104
+ if (!this.credentialId) {
105
+ throw new TetherError("Credential ID is required. Provide it in config or set TETHER_CREDENTIAL_ID environment variable.");
106
+ }
107
+ const keyPath = config.privateKeyPath || process.env.TETHER_PRIVATE_KEY_PATH;
108
+ this.privateKey = loadPrivateKey({
109
+ keyPath,
110
+ keyPem: config.privateKeyPem,
111
+ keyBuffer: config.privateKeyBuffer
112
+ });
113
+ this.baseUrl = config.baseUrl || "https://api.tether.name";
114
+ }
115
+ /**
116
+ * Request a challenge from the Tether API
117
+ */
118
+ async requestChallenge() {
119
+ try {
120
+ const response = await fetch(`${this.baseUrl}/challenge`, {
121
+ method: "POST",
122
+ headers: {
123
+ "Content-Type": "application/json"
124
+ }
125
+ });
126
+ if (!response.ok) {
127
+ const errorText = await response.text().catch(() => "Unknown error");
128
+ throw new TetherAPIError(
129
+ `Challenge request failed: ${response.status} ${response.statusText}`,
130
+ response.status,
131
+ errorText
132
+ );
133
+ }
134
+ const data = await response.json();
135
+ if (!data.code) {
136
+ throw new TetherAPIError("Invalid challenge response: missing code");
137
+ }
138
+ return data.code;
139
+ } catch (error) {
140
+ if (error instanceof TetherError) {
141
+ throw error;
142
+ }
143
+ throw new TetherAPIError(
144
+ `Failed to request challenge: ${error instanceof Error ? error.message : String(error)}`,
145
+ void 0,
146
+ void 0,
147
+ error instanceof Error ? error : void 0
148
+ );
149
+ }
150
+ }
151
+ /**
152
+ * Sign a challenge string
153
+ */
154
+ sign(challenge) {
155
+ return signChallenge(this.privateKey, challenge);
156
+ }
157
+ /**
158
+ * Submit proof for a challenge
159
+ */
160
+ async submitProof(challenge, proof) {
161
+ try {
162
+ const payload = {
163
+ challenge,
164
+ proof,
165
+ credentialId: this.credentialId
166
+ };
167
+ const response = await fetch(`${this.baseUrl}/challenge/verify`, {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/json"
171
+ },
172
+ body: JSON.stringify(payload)
173
+ });
174
+ if (!response.ok) {
175
+ const errorText = await response.text().catch(() => "Unknown error");
176
+ throw new TetherAPIError(
177
+ `Verification failed: ${response.status} ${response.statusText}`,
178
+ response.status,
179
+ errorText
180
+ );
181
+ }
182
+ const data = await response.json();
183
+ return {
184
+ verified: data.valid,
185
+ agentName: data.agentName,
186
+ verifyUrl: data.verifyUrl,
187
+ email: data.email,
188
+ registeredSince: data.registeredSince,
189
+ error: data.error,
190
+ challenge
191
+ };
192
+ } catch (error) {
193
+ if (error instanceof TetherError) {
194
+ throw error;
195
+ }
196
+ throw new TetherAPIError(
197
+ `Failed to submit proof: ${error instanceof Error ? error.message : String(error)}`,
198
+ void 0,
199
+ void 0,
200
+ error instanceof Error ? error : void 0
201
+ );
202
+ }
203
+ }
204
+ /**
205
+ * Perform complete verification in one call
206
+ */
207
+ async verify() {
208
+ try {
209
+ const challenge = await this.requestChallenge();
210
+ const proof = this.sign(challenge);
211
+ const result = await this.submitProof(challenge, proof);
212
+ if (!result.verified) {
213
+ throw new TetherVerificationError(
214
+ result.error || "Verification failed for unknown reason"
215
+ );
216
+ }
217
+ return result;
218
+ } catch (error) {
219
+ if (error instanceof TetherError) {
220
+ throw error;
221
+ }
222
+ throw new TetherVerificationError(
223
+ `Verification failed: ${error instanceof Error ? error.message : String(error)}`,
224
+ error instanceof Error ? error : void 0
225
+ );
226
+ }
227
+ }
228
+ };
229
+ export {
230
+ TetherAPIError,
231
+ TetherClient,
232
+ TetherError,
233
+ TetherVerificationError,
234
+ detectKeyFormat,
235
+ loadPrivateKey,
236
+ signChallenge
237
+ };
238
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/crypto.ts","../src/client.ts"],"sourcesContent":["/**\n * Base error class for all Tether-related errors\n */\nexport class TetherError extends Error {\n constructor(message: string, public readonly cause?: Error) {\n super(message);\n this.name = 'TetherError';\n \n // Maintain proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TetherError);\n }\n }\n}\n\n/**\n * Error thrown when verification fails\n */\nexport class TetherVerificationError extends TetherError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'TetherVerificationError';\n }\n}\n\n/**\n * Error thrown when API requests fail\n */\nexport class TetherAPIError extends TetherError {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly response?: string,\n cause?: Error\n ) {\n super(message, cause);\n this.name = 'TetherAPIError';\n }\n}","import { createSign, createPrivateKey, KeyObject } from 'crypto';\nimport { readFileSync } from 'fs';\nimport { TetherError } from './errors.js';\nimport type { KeyFormat } from './types.js';\n\n/**\n * Loads a private key from various sources\n */\nexport function loadPrivateKey(options: {\n keyPath?: string;\n keyPem?: string;\n keyBuffer?: Buffer;\n}): KeyObject {\n const { keyPath, keyPem, keyBuffer } = options;\n\n try {\n if (keyPem) {\n // PEM string provided directly\n return createPrivateKey(keyPem);\n }\n \n if (keyBuffer) {\n // DER buffer provided directly\n return createPrivateKey({\n key: keyBuffer,\n format: 'der',\n type: 'pkcs1'\n });\n }\n \n if (keyPath) {\n // Read from file - detect format by extension or content\n const keyData = readFileSync(keyPath);\n \n // Try to detect format\n if (keyPath.endsWith('.pem') || keyData.toString().includes('-----BEGIN')) {\n // PEM format\n return createPrivateKey(keyData);\n } else {\n // Assume DER format\n return createPrivateKey({\n key: keyData,\n format: 'der',\n type: 'pkcs1'\n });\n }\n }\n \n throw new TetherError('No private key provided');\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherError(\n `Failed to load private key: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Signs a challenge string using RSA-SHA256\n * Returns URL-safe base64 encoded signature (no padding)\n */\nexport function signChallenge(privateKey: KeyObject, challenge: string): string {\n try {\n const sign = createSign('SHA256');\n sign.update(challenge);\n sign.end();\n \n const signature = sign.sign(privateKey);\n \n // Convert to URL-safe base64 without padding\n return signature\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '');\n } catch (error) {\n throw new TetherError(\n `Failed to sign challenge: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Utility function to detect key format from file extension or content\n */\nexport function detectKeyFormat(keyPath: string): KeyFormat {\n if (keyPath.endsWith('.pem')) {\n return 'pem';\n }\n if (keyPath.endsWith('.der')) {\n return 'der';\n }\n \n // Try to read a small portion to detect format\n try {\n const keyData = readFileSync(keyPath, { encoding: 'utf8', flag: 'r' });\n if (keyData.includes('-----BEGIN')) {\n return 'pem';\n }\n } catch {\n // If we can't read as text, it's probably DER\n }\n \n return 'der';\n}","import { KeyObject } from 'crypto';\nimport { TetherError, TetherAPIError, TetherVerificationError } from './errors.js';\nimport { loadPrivateKey, signChallenge } from './crypto.js';\nimport type {\n TetherClientConfig,\n ChallengeResponse,\n VerificationRequest,\n VerificationResponse,\n VerificationResult\n} from './types.js';\n\n/**\n * TetherClient - Official SDK for tether.name agent identity verification\n */\nexport class TetherClient {\n private readonly credentialId: string;\n private readonly privateKey: KeyObject;\n private readonly baseUrl: string;\n\n constructor(config: TetherClientConfig) {\n // Get credential ID from config or environment\n this.credentialId = config.credentialId || process.env.TETHER_CREDENTIAL_ID || '';\n if (!this.credentialId) {\n throw new TetherError('Credential ID is required. Provide it in config or set TETHER_CREDENTIAL_ID environment variable.');\n }\n\n // Load private key\n const keyPath = config.privateKeyPath || process.env.TETHER_PRIVATE_KEY_PATH;\n this.privateKey = loadPrivateKey({\n keyPath,\n keyPem: config.privateKeyPem,\n keyBuffer: config.privateKeyBuffer\n });\n\n // Set base URL\n this.baseUrl = config.baseUrl || 'https://api.tether.name';\n }\n\n /**\n * Request a challenge from the Tether API\n */\n async requestChallenge(): Promise<string> {\n try {\n const response = await fetch(`${this.baseUrl}/challenge`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new TetherAPIError(\n `Challenge request failed: ${response.status} ${response.statusText}`,\n response.status,\n errorText\n );\n }\n\n const data = await response.json() as ChallengeResponse;\n \n if (!data.code) {\n throw new TetherAPIError('Invalid challenge response: missing code');\n }\n\n return data.code;\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherAPIError(\n `Failed to request challenge: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Sign a challenge string\n */\n sign(challenge: string): string {\n return signChallenge(this.privateKey, challenge);\n }\n\n /**\n * Submit proof for a challenge\n */\n async submitProof(challenge: string, proof: string): Promise<VerificationResult> {\n try {\n const payload: VerificationRequest = {\n challenge,\n proof,\n credentialId: this.credentialId\n };\n\n const response = await fetch(`${this.baseUrl}/challenge/verify`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new TetherAPIError(\n `Verification failed: ${response.status} ${response.statusText}`,\n response.status,\n errorText\n );\n }\n\n const data = await response.json() as VerificationResponse;\n\n // Convert API response to our result format\n return {\n verified: data.valid,\n agentName: data.agentName,\n verifyUrl: data.verifyUrl,\n email: data.email,\n registeredSince: data.registeredSince,\n error: data.error,\n challenge\n };\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherAPIError(\n `Failed to submit proof: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n undefined,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Perform complete verification in one call\n */\n async verify(): Promise<VerificationResult> {\n try {\n const challenge = await this.requestChallenge();\n const proof = this.sign(challenge);\n const result = await this.submitProof(challenge, proof);\n\n if (!result.verified) {\n throw new TetherVerificationError(\n result.error || 'Verification failed for unknown reason'\n );\n }\n\n return result;\n } catch (error) {\n if (error instanceof TetherError) {\n throw error;\n }\n throw new TetherVerificationError(\n `Verification failed: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n }\n}"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "tether-name",
3
+ "version": "1.0.0",
4
+ "description": "Official Node.js SDK for tether.name - AI agent identity verification",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "test": "vitest",
21
+ "test:run": "vitest run",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "tether",
26
+ "agent",
27
+ "verification",
28
+ "identity",
29
+ "crypto",
30
+ "rsa"
31
+ ],
32
+ "author": "Commit 451",
33
+ "license": "MIT",
34
+ "homepage": "https://tether.name",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/tether-name/tether-name-node.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/tether-name/tether-name-node/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.0.0",
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.0.0",
49
+ "vitest": "^1.0.0"
50
+ }
51
+ }