@socketfi/server 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 +1 -0
- package/README.md +199 -0
- package/dist/index.cjs +412 -0
- package/dist/index.d.cts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +388 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright 2026 SocketFi
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# @socketfi/server
|
|
2
|
+
|
|
3
|
+
Server-side SDK for verifying SocketFi authentication tokens.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @socketfi/server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Node.js 18+
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### ES Modules
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import express from "express";
|
|
27
|
+
import { SocketFi } from "@socketfi/server";
|
|
28
|
+
|
|
29
|
+
const app = express();
|
|
30
|
+
|
|
31
|
+
app.use(express.json());
|
|
32
|
+
|
|
33
|
+
const socketfi = new SocketFi({
|
|
34
|
+
clientId: process.env.APP_CLIENT_ID!,
|
|
35
|
+
secretKey: process.env.APP_SECRET_KEY!,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.post("/api/auth/verify", async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const authHeader = req.headers.authorization;
|
|
41
|
+
|
|
42
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
43
|
+
return res.status(401).json({
|
|
44
|
+
success: false,
|
|
45
|
+
error: "Authorization header missing",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const token = authHeader.split(" ")[1];
|
|
50
|
+
|
|
51
|
+
const session = await socketfi.verifyAuth(token);
|
|
52
|
+
|
|
53
|
+
return res.json({
|
|
54
|
+
success: true,
|
|
55
|
+
user: {
|
|
56
|
+
userId: session.userId,
|
|
57
|
+
username: session.username,
|
|
58
|
+
wallet: session.wallet,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return res.status(401).json({
|
|
63
|
+
success: false,
|
|
64
|
+
error: error instanceof Error ? error.message : "Invalid SocketFi token",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### CommonJS
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const { SocketFi } = require("@socketfi/server");
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Environment variables
|
|
81
|
+
|
|
82
|
+
```env
|
|
83
|
+
APP_CLIENT_ID=project_xxx
|
|
84
|
+
APP_SECRET_KEY=sk_live_xxx
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> `secretKey` must only be used on trusted backend servers.
|
|
88
|
+
> Never expose it in browsers, mobile apps, or client-side code.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Verification model
|
|
93
|
+
|
|
94
|
+
SocketFi access tokens are signed using RS256 asymmetric cryptography.
|
|
95
|
+
|
|
96
|
+
The SDK validates:
|
|
97
|
+
|
|
98
|
+
- Token signature
|
|
99
|
+
- Token expiration
|
|
100
|
+
- Issuer
|
|
101
|
+
- Audience (`clientId`)
|
|
102
|
+
- Token type (`access`)
|
|
103
|
+
- Signing algorithm
|
|
104
|
+
|
|
105
|
+
Verification flow:
|
|
106
|
+
|
|
107
|
+
- SocketFi private keys sign tokens.
|
|
108
|
+
- SocketFi public keys verify tokens.
|
|
109
|
+
- Your project's `secretKey` authenticates requests to the SocketFi key service.
|
|
110
|
+
- Public keys are cached in memory for improved performance.
|
|
111
|
+
- The SDK automatically refreshes public keys when:
|
|
112
|
+
|
|
113
|
+
- the cache expires
|
|
114
|
+
- token verification fails due to signature mismatch
|
|
115
|
+
- a JWT `kid` mismatch is detected
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Supported runtimes
|
|
120
|
+
|
|
121
|
+
- Node.js
|
|
122
|
+
- Express
|
|
123
|
+
- Next.js API routes
|
|
124
|
+
- NestJS
|
|
125
|
+
- Fastify
|
|
126
|
+
- Serverless functions
|
|
127
|
+
|
|
128
|
+
Edge runtimes are not currently supported.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Config
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const socketfi = new SocketFi({
|
|
136
|
+
clientId: "project_xxx",
|
|
137
|
+
secretKey: "sk_live_xxx",
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## API
|
|
144
|
+
|
|
145
|
+
### `verifyAuth(token)`
|
|
146
|
+
|
|
147
|
+
Verifies a SocketFi-issued access token and returns the authenticated session payload.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const session = await socketfi.verifyAuth(token);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Returned payload:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
{
|
|
157
|
+
userId: string;
|
|
158
|
+
accountId?: string;
|
|
159
|
+
username?: string;
|
|
160
|
+
wallet?: SocketFiWallet;
|
|
161
|
+
clientId: string;
|
|
162
|
+
origin: string;
|
|
163
|
+
expiresAt?: Date;
|
|
164
|
+
accessToken: string;
|
|
165
|
+
raw: SocketFiAccessTokenPayload;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`verifyAuth()` throws if:
|
|
170
|
+
|
|
171
|
+
- the token is invalid
|
|
172
|
+
- the token is expired
|
|
173
|
+
- the token audience does not match your `clientId`
|
|
174
|
+
- the token signature verification fails
|
|
175
|
+
- the token type is invalid
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `clearKeyCache()`
|
|
180
|
+
|
|
181
|
+
Clears the in-memory SocketFi public key cache.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
socketfi.clearKeyCache();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Stability
|
|
190
|
+
|
|
191
|
+
This SDK follows semantic versioning.
|
|
192
|
+
|
|
193
|
+
Breaking API changes are introduced only in major releases.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
Apache-2.0
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
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
|
+
SocketFi: () => SocketFi,
|
|
24
|
+
SocketFiError: () => SocketFiError
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/keys/public-key-cache.ts
|
|
29
|
+
var import_jose = require("jose");
|
|
30
|
+
|
|
31
|
+
// src/errors/SocketFiError.ts
|
|
32
|
+
var SocketFiError = class extends Error {
|
|
33
|
+
code;
|
|
34
|
+
statusCode;
|
|
35
|
+
details;
|
|
36
|
+
cause;
|
|
37
|
+
constructor(params) {
|
|
38
|
+
super(params.message);
|
|
39
|
+
this.name = "SocketFiError";
|
|
40
|
+
this.code = params.code;
|
|
41
|
+
if (params.statusCode !== void 0) {
|
|
42
|
+
this.statusCode = params.statusCode;
|
|
43
|
+
}
|
|
44
|
+
if (params.details !== void 0) {
|
|
45
|
+
this.details = params.details;
|
|
46
|
+
}
|
|
47
|
+
if (params.cause !== void 0) {
|
|
48
|
+
this.cause = params.cause;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/errors/error-codes.ts
|
|
54
|
+
var SOCKETFI_ERROR_CODES = {
|
|
55
|
+
CONFIG_ERROR: "config_error",
|
|
56
|
+
AUTH_TOKEN_REQUIRED: "auth_token_required",
|
|
57
|
+
INVALID_AUTH_TOKEN: "invalid_auth_token",
|
|
58
|
+
INVALID_TOKEN_TYPE: "invalid_token_type",
|
|
59
|
+
INVALID_CLIENT_ID: "invalid_client_id",
|
|
60
|
+
INVALID_ISSUER: "invalid_issuer",
|
|
61
|
+
INVALID_AUDIENCE: "invalid_audience",
|
|
62
|
+
PUBLIC_KEY_FETCH_FAILED: "public_key_fetch_failed",
|
|
63
|
+
PUBLIC_KEY_MISSING: "public_key_missing",
|
|
64
|
+
PUBLIC_KEY_IMPORT_FAILED: "public_key_import_failed",
|
|
65
|
+
PUBLIC_KEY_NOT_LOADED: "public_key_not_loaded",
|
|
66
|
+
NETWORK_ERROR: "network_error",
|
|
67
|
+
REQUEST_TIMEOUT: "request_timeout"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/http/request.ts
|
|
71
|
+
async function requestJson(url, options) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: options.method ?? "GET",
|
|
77
|
+
headers: {
|
|
78
|
+
accept: "application/json",
|
|
79
|
+
...options.headers
|
|
80
|
+
},
|
|
81
|
+
signal: controller.signal
|
|
82
|
+
});
|
|
83
|
+
const data = await res.json().catch(() => null);
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new SocketFiError({
|
|
86
|
+
code: SOCKETFI_ERROR_CODES.NETWORK_ERROR,
|
|
87
|
+
message: "socketfi_request_failed",
|
|
88
|
+
statusCode: res.status,
|
|
89
|
+
details: { url, response: data }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return data;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error?.name === "AbortError") {
|
|
95
|
+
throw new SocketFiError({
|
|
96
|
+
code: SOCKETFI_ERROR_CODES.REQUEST_TIMEOUT,
|
|
97
|
+
message: "socketfi_request_timeout",
|
|
98
|
+
statusCode: 408,
|
|
99
|
+
cause: error,
|
|
100
|
+
details: { url }
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (error instanceof SocketFiError) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
throw new SocketFiError({
|
|
107
|
+
code: SOCKETFI_ERROR_CODES.NETWORK_ERROR,
|
|
108
|
+
message: "socketfi_request_failed",
|
|
109
|
+
cause: error,
|
|
110
|
+
details: { url }
|
|
111
|
+
});
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/utils/normalize-url.ts
|
|
118
|
+
function normalizeBaseUrl(url) {
|
|
119
|
+
return url.replace(/\/+$/, "");
|
|
120
|
+
}
|
|
121
|
+
function joinUrl(baseUrl, path) {
|
|
122
|
+
const cleanBase = normalizeBaseUrl(baseUrl);
|
|
123
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
124
|
+
return `${cleanBase}${cleanPath}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/config/defaults.ts
|
|
128
|
+
var DEFAULT_SOCKETFI_API_URL = "https://api.socket.fi";
|
|
129
|
+
var DEFAULT_SOCKETFI_ISSUER = "https://socket.fi";
|
|
130
|
+
var DEFAULT_KEY_ENDPOINT = "/.well-known/socketfi-public-key";
|
|
131
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 8e3;
|
|
132
|
+
var DEFAULT_ALGORITHM = "RS256";
|
|
133
|
+
var DEFAULT_KEY_CACHE_TTL_MS = 60 * 60 * 24 * 1e3;
|
|
134
|
+
|
|
135
|
+
// src/keys/fetch-public-key.ts
|
|
136
|
+
async function fetchSocketFiPublicKey(params) {
|
|
137
|
+
const url = joinUrl(
|
|
138
|
+
params.apiUrl,
|
|
139
|
+
params.keyEndpoint ?? DEFAULT_KEY_ENDPOINT
|
|
140
|
+
);
|
|
141
|
+
const data = await requestJson(url, {
|
|
142
|
+
timeoutMs: params.timeoutMs,
|
|
143
|
+
headers: {
|
|
144
|
+
"x-socketfi-client-secret": params.clientSecret
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (!data?.publicKey || typeof data.publicKey !== "string") {
|
|
148
|
+
throw new SocketFiError({
|
|
149
|
+
code: SOCKETFI_ERROR_CODES.PUBLIC_KEY_MISSING,
|
|
150
|
+
message: "socketfi_public_key_missing",
|
|
151
|
+
statusCode: 502
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (data.alg !== "RS256") {
|
|
155
|
+
throw new SocketFiError({
|
|
156
|
+
code: SOCKETFI_ERROR_CODES.PUBLIC_KEY_FETCH_FAILED,
|
|
157
|
+
message: "unsupported_socketfi_public_key_algorithm",
|
|
158
|
+
statusCode: 502,
|
|
159
|
+
details: { alg: data.alg }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/keys/public-key-cache.ts
|
|
166
|
+
var PublicKeyCache = class {
|
|
167
|
+
constructor(config) {
|
|
168
|
+
this.config = config;
|
|
169
|
+
}
|
|
170
|
+
config;
|
|
171
|
+
cached = null;
|
|
172
|
+
refreshPromise = null;
|
|
173
|
+
async get() {
|
|
174
|
+
if (this.cached && this.cached.expiresAt > Date.now()) {
|
|
175
|
+
return this.cached;
|
|
176
|
+
}
|
|
177
|
+
return this.refresh();
|
|
178
|
+
}
|
|
179
|
+
async refresh() {
|
|
180
|
+
if (!this.refreshPromise) {
|
|
181
|
+
this.refreshPromise = this.fetchAndImport().finally(() => {
|
|
182
|
+
this.refreshPromise = null;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return this.refreshPromise;
|
|
186
|
+
}
|
|
187
|
+
clear() {
|
|
188
|
+
this.cached = null;
|
|
189
|
+
this.refreshPromise = null;
|
|
190
|
+
}
|
|
191
|
+
async fetchAndImport() {
|
|
192
|
+
const response = await fetchSocketFiPublicKey({
|
|
193
|
+
apiUrl: this.config.apiUrl,
|
|
194
|
+
clientSecret: this.config.clientSecret,
|
|
195
|
+
timeoutMs: this.config.timeoutMs,
|
|
196
|
+
...this.config.keyEndpoint ? { keyEndpoint: this.config.keyEndpoint } : {}
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
const importedKey = await (0, import_jose.importSPKI)(
|
|
200
|
+
response.publicKey,
|
|
201
|
+
DEFAULT_ALGORITHM
|
|
202
|
+
);
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
this.cached = {
|
|
205
|
+
kid: response.kid,
|
|
206
|
+
alg: response.alg,
|
|
207
|
+
publicKeyPem: response.publicKey,
|
|
208
|
+
importedKey,
|
|
209
|
+
fetchedAt: now,
|
|
210
|
+
expiresAt: now + this.config.cacheTtlMs
|
|
211
|
+
};
|
|
212
|
+
return this.cached;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw new SocketFiError({
|
|
215
|
+
code: SOCKETFI_ERROR_CODES.PUBLIC_KEY_IMPORT_FAILED,
|
|
216
|
+
message: "socketfi_public_key_import_failed",
|
|
217
|
+
statusCode: 502,
|
|
218
|
+
cause: error
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/auth/verifyAuth.ts
|
|
225
|
+
var import_jose2 = require("jose");
|
|
226
|
+
async function verifyAuth(params) {
|
|
227
|
+
const token = params.token;
|
|
228
|
+
if (!token || typeof token !== "string") {
|
|
229
|
+
throw new SocketFiError({
|
|
230
|
+
code: SOCKETFI_ERROR_CODES.AUTH_TOKEN_REQUIRED,
|
|
231
|
+
message: "auth_token_required",
|
|
232
|
+
statusCode: 401
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (params.options?.forceRefreshKey) {
|
|
236
|
+
await params.keyCache.refresh();
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const result = await verifyWithCache(params);
|
|
240
|
+
return {
|
|
241
|
+
...result,
|
|
242
|
+
accessToken: token
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (!shouldRefreshKey(error)) {
|
|
246
|
+
throw normalizeVerifyError(error);
|
|
247
|
+
}
|
|
248
|
+
await params.keyCache.refresh();
|
|
249
|
+
try {
|
|
250
|
+
const result = await verifyWithCache(params);
|
|
251
|
+
return {
|
|
252
|
+
...result,
|
|
253
|
+
accessToken: token
|
|
254
|
+
};
|
|
255
|
+
} catch (retryError) {
|
|
256
|
+
throw normalizeVerifyError(retryError, true);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function verifyWithCache(params) {
|
|
261
|
+
const header = (0, import_jose2.decodeProtectedHeader)(params.token);
|
|
262
|
+
if (header.alg !== DEFAULT_ALGORITHM) {
|
|
263
|
+
throw new SocketFiError({
|
|
264
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
265
|
+
message: "unsupported_auth_token_algorithm",
|
|
266
|
+
statusCode: 401,
|
|
267
|
+
details: { alg: header.alg }
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const cachedKey = await params.keyCache.get();
|
|
271
|
+
if (header.kid && cachedKey.kid && header.kid !== cachedKey.kid) {
|
|
272
|
+
await params.keyCache.refresh();
|
|
273
|
+
}
|
|
274
|
+
const key = (await params.keyCache.get()).importedKey;
|
|
275
|
+
const { payload } = await (0, import_jose2.jwtVerify)(params.token, key, {
|
|
276
|
+
issuer: params.issuer,
|
|
277
|
+
audience: params.clientId,
|
|
278
|
+
algorithms: [DEFAULT_ALGORITHM]
|
|
279
|
+
});
|
|
280
|
+
return normalizeAccessPayload(payload, params.clientId);
|
|
281
|
+
}
|
|
282
|
+
function normalizeAccessPayload(payload, clientId) {
|
|
283
|
+
const typed = payload;
|
|
284
|
+
if (typed.type !== "access") {
|
|
285
|
+
throw new SocketFiError({
|
|
286
|
+
code: SOCKETFI_ERROR_CODES.INVALID_TOKEN_TYPE,
|
|
287
|
+
message: "invalid_token_type",
|
|
288
|
+
statusCode: 401,
|
|
289
|
+
details: { type: typed.type }
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (typed.clientId !== clientId) {
|
|
293
|
+
throw new SocketFiError({
|
|
294
|
+
code: SOCKETFI_ERROR_CODES.INVALID_CLIENT_ID,
|
|
295
|
+
message: "invalid_client_id",
|
|
296
|
+
statusCode: 401,
|
|
297
|
+
details: { expected: clientId, received: typed.clientId }
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (!typed.sub || typeof typed.sub !== "string") {
|
|
301
|
+
throw new SocketFiError({
|
|
302
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
303
|
+
message: "invalid_subject",
|
|
304
|
+
statusCode: 401
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (!typed.origin || typeof typed.origin !== "string") {
|
|
308
|
+
throw new SocketFiError({
|
|
309
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
310
|
+
message: "invalid_origin",
|
|
311
|
+
statusCode: 401
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
userId: typed.sub,
|
|
316
|
+
...typeof typed.accountId === "string" ? { accountId: typed.accountId } : {},
|
|
317
|
+
...typeof typed.username === "string" ? { username: typed.username } : {},
|
|
318
|
+
wallet: typed.wallet,
|
|
319
|
+
clientId: typed.clientId,
|
|
320
|
+
origin: typed.origin,
|
|
321
|
+
...typeof typed.exp === "number" ? { expiresAt: new Date(typed.exp * 1e3) } : {},
|
|
322
|
+
raw: typed
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function shouldRefreshKey(error) {
|
|
326
|
+
return error instanceof import_jose2.errors.JWSSignatureVerificationFailed || error instanceof import_jose2.errors.JWSInvalid || error?.code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" || String(error?.message || "").includes(
|
|
327
|
+
"signature verification failed"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
function normalizeVerifyError(error, afterRefresh = false) {
|
|
331
|
+
if (error instanceof SocketFiError) return error;
|
|
332
|
+
if (afterRefresh && (error instanceof import_jose2.errors.JWSSignatureVerificationFailed || error?.code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED")) {
|
|
333
|
+
return new SocketFiError({
|
|
334
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
335
|
+
message: "socketfi_token_signature_mismatch_after_key_refresh",
|
|
336
|
+
statusCode: 401
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (error instanceof import_jose2.errors.JWTExpired) {
|
|
340
|
+
return new SocketFiError({
|
|
341
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
342
|
+
message: "auth_token_expired",
|
|
343
|
+
statusCode: 401,
|
|
344
|
+
details: {
|
|
345
|
+
claim: "exp"
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (error instanceof import_jose2.errors.JWTClaimValidationFailed) {
|
|
350
|
+
return new SocketFiError({
|
|
351
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
352
|
+
message: "invalid_auth_token_claim",
|
|
353
|
+
statusCode: 401,
|
|
354
|
+
details: {
|
|
355
|
+
claim: error.claim,
|
|
356
|
+
reason: error.reason
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return new SocketFiError({
|
|
361
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
362
|
+
message: "invalid_auth_token",
|
|
363
|
+
statusCode: 401
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/utils/assert.ts
|
|
368
|
+
function assertNonEmptyString(value, name) {
|
|
369
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
370
|
+
throw new SocketFiError({
|
|
371
|
+
code: SOCKETFI_ERROR_CODES.CONFIG_ERROR,
|
|
372
|
+
message: `${name}_required`,
|
|
373
|
+
statusCode: 400,
|
|
374
|
+
details: { field: name }
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/socketfi.ts
|
|
380
|
+
var SocketFi = class {
|
|
381
|
+
clientId;
|
|
382
|
+
secretKey;
|
|
383
|
+
keyCache;
|
|
384
|
+
constructor(config) {
|
|
385
|
+
assertNonEmptyString(config.clientId, "clientId");
|
|
386
|
+
assertNonEmptyString(config.secretKey, "secretKey");
|
|
387
|
+
this.clientId = config.clientId;
|
|
388
|
+
this.secretKey = config.secretKey;
|
|
389
|
+
this.keyCache = new PublicKeyCache({
|
|
390
|
+
apiUrl: DEFAULT_SOCKETFI_API_URL,
|
|
391
|
+
clientSecret: this.secretKey,
|
|
392
|
+
timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
|
|
393
|
+
cacheTtlMs: DEFAULT_KEY_CACHE_TTL_MS
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async verifyAuth(token) {
|
|
397
|
+
return verifyAuth({
|
|
398
|
+
token,
|
|
399
|
+
clientId: this.clientId,
|
|
400
|
+
issuer: DEFAULT_SOCKETFI_ISSUER,
|
|
401
|
+
keyCache: this.keyCache
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
clearKeyCache() {
|
|
405
|
+
this.keyCache.clear();
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
409
|
+
0 && (module.exports = {
|
|
410
|
+
SocketFi,
|
|
411
|
+
SocketFiError
|
|
412
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type SocketFiConfig = {
|
|
2
|
+
clientId: string;
|
|
3
|
+
secretKey: string;
|
|
4
|
+
};
|
|
5
|
+
type SocketFiAccessTokenPayload = {
|
|
6
|
+
sub: string;
|
|
7
|
+
accountId?: string;
|
|
8
|
+
username?: string;
|
|
9
|
+
wallet?: unknown;
|
|
10
|
+
clientId: string;
|
|
11
|
+
origin: string;
|
|
12
|
+
type: "access";
|
|
13
|
+
iat?: number;
|
|
14
|
+
exp?: number;
|
|
15
|
+
iss?: string;
|
|
16
|
+
aud?: string | string[];
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
type VerifyAuthResult = {
|
|
20
|
+
userId: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
username?: string;
|
|
23
|
+
wallet: unknown;
|
|
24
|
+
clientId: string;
|
|
25
|
+
origin: string;
|
|
26
|
+
accessToken: string;
|
|
27
|
+
expiresAt?: Date;
|
|
28
|
+
raw: SocketFiAccessTokenPayload;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
declare class SocketFi {
|
|
32
|
+
private readonly clientId;
|
|
33
|
+
private readonly secretKey;
|
|
34
|
+
private readonly keyCache;
|
|
35
|
+
constructor(config: SocketFiConfig);
|
|
36
|
+
verifyAuth(token: string): Promise<VerifyAuthResult>;
|
|
37
|
+
clearKeyCache(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare const SOCKETFI_ERROR_CODES: {
|
|
41
|
+
readonly CONFIG_ERROR: "config_error";
|
|
42
|
+
readonly AUTH_TOKEN_REQUIRED: "auth_token_required";
|
|
43
|
+
readonly INVALID_AUTH_TOKEN: "invalid_auth_token";
|
|
44
|
+
readonly INVALID_TOKEN_TYPE: "invalid_token_type";
|
|
45
|
+
readonly INVALID_CLIENT_ID: "invalid_client_id";
|
|
46
|
+
readonly INVALID_ISSUER: "invalid_issuer";
|
|
47
|
+
readonly INVALID_AUDIENCE: "invalid_audience";
|
|
48
|
+
readonly PUBLIC_KEY_FETCH_FAILED: "public_key_fetch_failed";
|
|
49
|
+
readonly PUBLIC_KEY_MISSING: "public_key_missing";
|
|
50
|
+
readonly PUBLIC_KEY_IMPORT_FAILED: "public_key_import_failed";
|
|
51
|
+
readonly PUBLIC_KEY_NOT_LOADED: "public_key_not_loaded";
|
|
52
|
+
readonly NETWORK_ERROR: "network_error";
|
|
53
|
+
readonly REQUEST_TIMEOUT: "request_timeout";
|
|
54
|
+
};
|
|
55
|
+
type SocketFiErrorCode = (typeof SOCKETFI_ERROR_CODES)[keyof typeof SOCKETFI_ERROR_CODES];
|
|
56
|
+
|
|
57
|
+
declare class SocketFiError extends Error {
|
|
58
|
+
readonly code: SocketFiErrorCode;
|
|
59
|
+
readonly statusCode?: number;
|
|
60
|
+
readonly details?: Record<string, unknown>;
|
|
61
|
+
readonly cause?: unknown;
|
|
62
|
+
constructor(params: {
|
|
63
|
+
code: SocketFiErrorCode;
|
|
64
|
+
message: string;
|
|
65
|
+
statusCode?: number;
|
|
66
|
+
details?: Record<string, unknown>;
|
|
67
|
+
cause?: unknown;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { SocketFi, type SocketFiConfig, SocketFiError, type VerifyAuthResult };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type SocketFiConfig = {
|
|
2
|
+
clientId: string;
|
|
3
|
+
secretKey: string;
|
|
4
|
+
};
|
|
5
|
+
type SocketFiAccessTokenPayload = {
|
|
6
|
+
sub: string;
|
|
7
|
+
accountId?: string;
|
|
8
|
+
username?: string;
|
|
9
|
+
wallet?: unknown;
|
|
10
|
+
clientId: string;
|
|
11
|
+
origin: string;
|
|
12
|
+
type: "access";
|
|
13
|
+
iat?: number;
|
|
14
|
+
exp?: number;
|
|
15
|
+
iss?: string;
|
|
16
|
+
aud?: string | string[];
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
type VerifyAuthResult = {
|
|
20
|
+
userId: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
username?: string;
|
|
23
|
+
wallet: unknown;
|
|
24
|
+
clientId: string;
|
|
25
|
+
origin: string;
|
|
26
|
+
accessToken: string;
|
|
27
|
+
expiresAt?: Date;
|
|
28
|
+
raw: SocketFiAccessTokenPayload;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
declare class SocketFi {
|
|
32
|
+
private readonly clientId;
|
|
33
|
+
private readonly secretKey;
|
|
34
|
+
private readonly keyCache;
|
|
35
|
+
constructor(config: SocketFiConfig);
|
|
36
|
+
verifyAuth(token: string): Promise<VerifyAuthResult>;
|
|
37
|
+
clearKeyCache(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare const SOCKETFI_ERROR_CODES: {
|
|
41
|
+
readonly CONFIG_ERROR: "config_error";
|
|
42
|
+
readonly AUTH_TOKEN_REQUIRED: "auth_token_required";
|
|
43
|
+
readonly INVALID_AUTH_TOKEN: "invalid_auth_token";
|
|
44
|
+
readonly INVALID_TOKEN_TYPE: "invalid_token_type";
|
|
45
|
+
readonly INVALID_CLIENT_ID: "invalid_client_id";
|
|
46
|
+
readonly INVALID_ISSUER: "invalid_issuer";
|
|
47
|
+
readonly INVALID_AUDIENCE: "invalid_audience";
|
|
48
|
+
readonly PUBLIC_KEY_FETCH_FAILED: "public_key_fetch_failed";
|
|
49
|
+
readonly PUBLIC_KEY_MISSING: "public_key_missing";
|
|
50
|
+
readonly PUBLIC_KEY_IMPORT_FAILED: "public_key_import_failed";
|
|
51
|
+
readonly PUBLIC_KEY_NOT_LOADED: "public_key_not_loaded";
|
|
52
|
+
readonly NETWORK_ERROR: "network_error";
|
|
53
|
+
readonly REQUEST_TIMEOUT: "request_timeout";
|
|
54
|
+
};
|
|
55
|
+
type SocketFiErrorCode = (typeof SOCKETFI_ERROR_CODES)[keyof typeof SOCKETFI_ERROR_CODES];
|
|
56
|
+
|
|
57
|
+
declare class SocketFiError extends Error {
|
|
58
|
+
readonly code: SocketFiErrorCode;
|
|
59
|
+
readonly statusCode?: number;
|
|
60
|
+
readonly details?: Record<string, unknown>;
|
|
61
|
+
readonly cause?: unknown;
|
|
62
|
+
constructor(params: {
|
|
63
|
+
code: SocketFiErrorCode;
|
|
64
|
+
message: string;
|
|
65
|
+
statusCode?: number;
|
|
66
|
+
details?: Record<string, unknown>;
|
|
67
|
+
cause?: unknown;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { SocketFi, type SocketFiConfig, SocketFiError, type VerifyAuthResult };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// src/keys/public-key-cache.ts
|
|
2
|
+
import { importSPKI } from "jose";
|
|
3
|
+
|
|
4
|
+
// src/errors/SocketFiError.ts
|
|
5
|
+
var SocketFiError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
statusCode;
|
|
8
|
+
details;
|
|
9
|
+
cause;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
super(params.message);
|
|
12
|
+
this.name = "SocketFiError";
|
|
13
|
+
this.code = params.code;
|
|
14
|
+
if (params.statusCode !== void 0) {
|
|
15
|
+
this.statusCode = params.statusCode;
|
|
16
|
+
}
|
|
17
|
+
if (params.details !== void 0) {
|
|
18
|
+
this.details = params.details;
|
|
19
|
+
}
|
|
20
|
+
if (params.cause !== void 0) {
|
|
21
|
+
this.cause = params.cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/errors/error-codes.ts
|
|
27
|
+
var SOCKETFI_ERROR_CODES = {
|
|
28
|
+
CONFIG_ERROR: "config_error",
|
|
29
|
+
AUTH_TOKEN_REQUIRED: "auth_token_required",
|
|
30
|
+
INVALID_AUTH_TOKEN: "invalid_auth_token",
|
|
31
|
+
INVALID_TOKEN_TYPE: "invalid_token_type",
|
|
32
|
+
INVALID_CLIENT_ID: "invalid_client_id",
|
|
33
|
+
INVALID_ISSUER: "invalid_issuer",
|
|
34
|
+
INVALID_AUDIENCE: "invalid_audience",
|
|
35
|
+
PUBLIC_KEY_FETCH_FAILED: "public_key_fetch_failed",
|
|
36
|
+
PUBLIC_KEY_MISSING: "public_key_missing",
|
|
37
|
+
PUBLIC_KEY_IMPORT_FAILED: "public_key_import_failed",
|
|
38
|
+
PUBLIC_KEY_NOT_LOADED: "public_key_not_loaded",
|
|
39
|
+
NETWORK_ERROR: "network_error",
|
|
40
|
+
REQUEST_TIMEOUT: "request_timeout"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/http/request.ts
|
|
44
|
+
async function requestJson(url, options) {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: options.method ?? "GET",
|
|
50
|
+
headers: {
|
|
51
|
+
accept: "application/json",
|
|
52
|
+
...options.headers
|
|
53
|
+
},
|
|
54
|
+
signal: controller.signal
|
|
55
|
+
});
|
|
56
|
+
const data = await res.json().catch(() => null);
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new SocketFiError({
|
|
59
|
+
code: SOCKETFI_ERROR_CODES.NETWORK_ERROR,
|
|
60
|
+
message: "socketfi_request_failed",
|
|
61
|
+
statusCode: res.status,
|
|
62
|
+
details: { url, response: data }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return data;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error?.name === "AbortError") {
|
|
68
|
+
throw new SocketFiError({
|
|
69
|
+
code: SOCKETFI_ERROR_CODES.REQUEST_TIMEOUT,
|
|
70
|
+
message: "socketfi_request_timeout",
|
|
71
|
+
statusCode: 408,
|
|
72
|
+
cause: error,
|
|
73
|
+
details: { url }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (error instanceof SocketFiError) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
throw new SocketFiError({
|
|
80
|
+
code: SOCKETFI_ERROR_CODES.NETWORK_ERROR,
|
|
81
|
+
message: "socketfi_request_failed",
|
|
82
|
+
cause: error,
|
|
83
|
+
details: { url }
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/utils/normalize-url.ts
|
|
91
|
+
function normalizeBaseUrl(url) {
|
|
92
|
+
return url.replace(/\/+$/, "");
|
|
93
|
+
}
|
|
94
|
+
function joinUrl(baseUrl, path) {
|
|
95
|
+
const cleanBase = normalizeBaseUrl(baseUrl);
|
|
96
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
97
|
+
return `${cleanBase}${cleanPath}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/config/defaults.ts
|
|
101
|
+
var DEFAULT_SOCKETFI_API_URL = "https://api.socket.fi";
|
|
102
|
+
var DEFAULT_SOCKETFI_ISSUER = "https://socket.fi";
|
|
103
|
+
var DEFAULT_KEY_ENDPOINT = "/.well-known/socketfi-public-key";
|
|
104
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 8e3;
|
|
105
|
+
var DEFAULT_ALGORITHM = "RS256";
|
|
106
|
+
var DEFAULT_KEY_CACHE_TTL_MS = 60 * 60 * 24 * 1e3;
|
|
107
|
+
|
|
108
|
+
// src/keys/fetch-public-key.ts
|
|
109
|
+
async function fetchSocketFiPublicKey(params) {
|
|
110
|
+
const url = joinUrl(
|
|
111
|
+
params.apiUrl,
|
|
112
|
+
params.keyEndpoint ?? DEFAULT_KEY_ENDPOINT
|
|
113
|
+
);
|
|
114
|
+
const data = await requestJson(url, {
|
|
115
|
+
timeoutMs: params.timeoutMs,
|
|
116
|
+
headers: {
|
|
117
|
+
"x-socketfi-client-secret": params.clientSecret
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (!data?.publicKey || typeof data.publicKey !== "string") {
|
|
121
|
+
throw new SocketFiError({
|
|
122
|
+
code: SOCKETFI_ERROR_CODES.PUBLIC_KEY_MISSING,
|
|
123
|
+
message: "socketfi_public_key_missing",
|
|
124
|
+
statusCode: 502
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (data.alg !== "RS256") {
|
|
128
|
+
throw new SocketFiError({
|
|
129
|
+
code: SOCKETFI_ERROR_CODES.PUBLIC_KEY_FETCH_FAILED,
|
|
130
|
+
message: "unsupported_socketfi_public_key_algorithm",
|
|
131
|
+
statusCode: 502,
|
|
132
|
+
details: { alg: data.alg }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/keys/public-key-cache.ts
|
|
139
|
+
var PublicKeyCache = class {
|
|
140
|
+
constructor(config) {
|
|
141
|
+
this.config = config;
|
|
142
|
+
}
|
|
143
|
+
config;
|
|
144
|
+
cached = null;
|
|
145
|
+
refreshPromise = null;
|
|
146
|
+
async get() {
|
|
147
|
+
if (this.cached && this.cached.expiresAt > Date.now()) {
|
|
148
|
+
return this.cached;
|
|
149
|
+
}
|
|
150
|
+
return this.refresh();
|
|
151
|
+
}
|
|
152
|
+
async refresh() {
|
|
153
|
+
if (!this.refreshPromise) {
|
|
154
|
+
this.refreshPromise = this.fetchAndImport().finally(() => {
|
|
155
|
+
this.refreshPromise = null;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return this.refreshPromise;
|
|
159
|
+
}
|
|
160
|
+
clear() {
|
|
161
|
+
this.cached = null;
|
|
162
|
+
this.refreshPromise = null;
|
|
163
|
+
}
|
|
164
|
+
async fetchAndImport() {
|
|
165
|
+
const response = await fetchSocketFiPublicKey({
|
|
166
|
+
apiUrl: this.config.apiUrl,
|
|
167
|
+
clientSecret: this.config.clientSecret,
|
|
168
|
+
timeoutMs: this.config.timeoutMs,
|
|
169
|
+
...this.config.keyEndpoint ? { keyEndpoint: this.config.keyEndpoint } : {}
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
const importedKey = await importSPKI(
|
|
173
|
+
response.publicKey,
|
|
174
|
+
DEFAULT_ALGORITHM
|
|
175
|
+
);
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
this.cached = {
|
|
178
|
+
kid: response.kid,
|
|
179
|
+
alg: response.alg,
|
|
180
|
+
publicKeyPem: response.publicKey,
|
|
181
|
+
importedKey,
|
|
182
|
+
fetchedAt: now,
|
|
183
|
+
expiresAt: now + this.config.cacheTtlMs
|
|
184
|
+
};
|
|
185
|
+
return this.cached;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new SocketFiError({
|
|
188
|
+
code: SOCKETFI_ERROR_CODES.PUBLIC_KEY_IMPORT_FAILED,
|
|
189
|
+
message: "socketfi_public_key_import_failed",
|
|
190
|
+
statusCode: 502,
|
|
191
|
+
cause: error
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/auth/verifyAuth.ts
|
|
198
|
+
import {
|
|
199
|
+
decodeProtectedHeader,
|
|
200
|
+
errors,
|
|
201
|
+
jwtVerify
|
|
202
|
+
} from "jose";
|
|
203
|
+
async function verifyAuth(params) {
|
|
204
|
+
const token = params.token;
|
|
205
|
+
if (!token || typeof token !== "string") {
|
|
206
|
+
throw new SocketFiError({
|
|
207
|
+
code: SOCKETFI_ERROR_CODES.AUTH_TOKEN_REQUIRED,
|
|
208
|
+
message: "auth_token_required",
|
|
209
|
+
statusCode: 401
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (params.options?.forceRefreshKey) {
|
|
213
|
+
await params.keyCache.refresh();
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const result = await verifyWithCache(params);
|
|
217
|
+
return {
|
|
218
|
+
...result,
|
|
219
|
+
accessToken: token
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (!shouldRefreshKey(error)) {
|
|
223
|
+
throw normalizeVerifyError(error);
|
|
224
|
+
}
|
|
225
|
+
await params.keyCache.refresh();
|
|
226
|
+
try {
|
|
227
|
+
const result = await verifyWithCache(params);
|
|
228
|
+
return {
|
|
229
|
+
...result,
|
|
230
|
+
accessToken: token
|
|
231
|
+
};
|
|
232
|
+
} catch (retryError) {
|
|
233
|
+
throw normalizeVerifyError(retryError, true);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function verifyWithCache(params) {
|
|
238
|
+
const header = decodeProtectedHeader(params.token);
|
|
239
|
+
if (header.alg !== DEFAULT_ALGORITHM) {
|
|
240
|
+
throw new SocketFiError({
|
|
241
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
242
|
+
message: "unsupported_auth_token_algorithm",
|
|
243
|
+
statusCode: 401,
|
|
244
|
+
details: { alg: header.alg }
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const cachedKey = await params.keyCache.get();
|
|
248
|
+
if (header.kid && cachedKey.kid && header.kid !== cachedKey.kid) {
|
|
249
|
+
await params.keyCache.refresh();
|
|
250
|
+
}
|
|
251
|
+
const key = (await params.keyCache.get()).importedKey;
|
|
252
|
+
const { payload } = await jwtVerify(params.token, key, {
|
|
253
|
+
issuer: params.issuer,
|
|
254
|
+
audience: params.clientId,
|
|
255
|
+
algorithms: [DEFAULT_ALGORITHM]
|
|
256
|
+
});
|
|
257
|
+
return normalizeAccessPayload(payload, params.clientId);
|
|
258
|
+
}
|
|
259
|
+
function normalizeAccessPayload(payload, clientId) {
|
|
260
|
+
const typed = payload;
|
|
261
|
+
if (typed.type !== "access") {
|
|
262
|
+
throw new SocketFiError({
|
|
263
|
+
code: SOCKETFI_ERROR_CODES.INVALID_TOKEN_TYPE,
|
|
264
|
+
message: "invalid_token_type",
|
|
265
|
+
statusCode: 401,
|
|
266
|
+
details: { type: typed.type }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (typed.clientId !== clientId) {
|
|
270
|
+
throw new SocketFiError({
|
|
271
|
+
code: SOCKETFI_ERROR_CODES.INVALID_CLIENT_ID,
|
|
272
|
+
message: "invalid_client_id",
|
|
273
|
+
statusCode: 401,
|
|
274
|
+
details: { expected: clientId, received: typed.clientId }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (!typed.sub || typeof typed.sub !== "string") {
|
|
278
|
+
throw new SocketFiError({
|
|
279
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
280
|
+
message: "invalid_subject",
|
|
281
|
+
statusCode: 401
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (!typed.origin || typeof typed.origin !== "string") {
|
|
285
|
+
throw new SocketFiError({
|
|
286
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
287
|
+
message: "invalid_origin",
|
|
288
|
+
statusCode: 401
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
userId: typed.sub,
|
|
293
|
+
...typeof typed.accountId === "string" ? { accountId: typed.accountId } : {},
|
|
294
|
+
...typeof typed.username === "string" ? { username: typed.username } : {},
|
|
295
|
+
wallet: typed.wallet,
|
|
296
|
+
clientId: typed.clientId,
|
|
297
|
+
origin: typed.origin,
|
|
298
|
+
...typeof typed.exp === "number" ? { expiresAt: new Date(typed.exp * 1e3) } : {},
|
|
299
|
+
raw: typed
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function shouldRefreshKey(error) {
|
|
303
|
+
return error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWSInvalid || error?.code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" || String(error?.message || "").includes(
|
|
304
|
+
"signature verification failed"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
function normalizeVerifyError(error, afterRefresh = false) {
|
|
308
|
+
if (error instanceof SocketFiError) return error;
|
|
309
|
+
if (afterRefresh && (error instanceof errors.JWSSignatureVerificationFailed || error?.code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED")) {
|
|
310
|
+
return new SocketFiError({
|
|
311
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
312
|
+
message: "socketfi_token_signature_mismatch_after_key_refresh",
|
|
313
|
+
statusCode: 401
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (error instanceof errors.JWTExpired) {
|
|
317
|
+
return new SocketFiError({
|
|
318
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
319
|
+
message: "auth_token_expired",
|
|
320
|
+
statusCode: 401,
|
|
321
|
+
details: {
|
|
322
|
+
claim: "exp"
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (error instanceof errors.JWTClaimValidationFailed) {
|
|
327
|
+
return new SocketFiError({
|
|
328
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
329
|
+
message: "invalid_auth_token_claim",
|
|
330
|
+
statusCode: 401,
|
|
331
|
+
details: {
|
|
332
|
+
claim: error.claim,
|
|
333
|
+
reason: error.reason
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return new SocketFiError({
|
|
338
|
+
code: SOCKETFI_ERROR_CODES.INVALID_AUTH_TOKEN,
|
|
339
|
+
message: "invalid_auth_token",
|
|
340
|
+
statusCode: 401
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/utils/assert.ts
|
|
345
|
+
function assertNonEmptyString(value, name) {
|
|
346
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
347
|
+
throw new SocketFiError({
|
|
348
|
+
code: SOCKETFI_ERROR_CODES.CONFIG_ERROR,
|
|
349
|
+
message: `${name}_required`,
|
|
350
|
+
statusCode: 400,
|
|
351
|
+
details: { field: name }
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/socketfi.ts
|
|
357
|
+
var SocketFi = class {
|
|
358
|
+
clientId;
|
|
359
|
+
secretKey;
|
|
360
|
+
keyCache;
|
|
361
|
+
constructor(config) {
|
|
362
|
+
assertNonEmptyString(config.clientId, "clientId");
|
|
363
|
+
assertNonEmptyString(config.secretKey, "secretKey");
|
|
364
|
+
this.clientId = config.clientId;
|
|
365
|
+
this.secretKey = config.secretKey;
|
|
366
|
+
this.keyCache = new PublicKeyCache({
|
|
367
|
+
apiUrl: DEFAULT_SOCKETFI_API_URL,
|
|
368
|
+
clientSecret: this.secretKey,
|
|
369
|
+
timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
|
|
370
|
+
cacheTtlMs: DEFAULT_KEY_CACHE_TTL_MS
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async verifyAuth(token) {
|
|
374
|
+
return verifyAuth({
|
|
375
|
+
token,
|
|
376
|
+
clientId: this.clientId,
|
|
377
|
+
issuer: DEFAULT_SOCKETFI_ISSUER,
|
|
378
|
+
keyCache: this.keyCache
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
clearKeyCache() {
|
|
382
|
+
this.keyCache.clear();
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
export {
|
|
386
|
+
SocketFi,
|
|
387
|
+
SocketFiError
|
|
388
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@socketfi/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Server-side SDK for verifying SocketFi authentication tokens.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "SocketFi",
|
|
7
|
+
"homepage": "https://socket.fi",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"socketfi",
|
|
10
|
+
"authentication",
|
|
11
|
+
"auth",
|
|
12
|
+
"jwt",
|
|
13
|
+
"passkeys",
|
|
14
|
+
"webauthn",
|
|
15
|
+
"wallet",
|
|
16
|
+
"embedded-wallet",
|
|
17
|
+
"stellar",
|
|
18
|
+
"soroban"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"require": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
42
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"jose": "^6.2.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
}
|
|
56
|
+
}
|