@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 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
+ });
@@ -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 };
@@ -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
+ }