@stefanobalocco/honosignedrequests 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.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2025, Stefano Balocco <stefano.balocco@gmail.com>
2
+ All rights reserved.
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+ * Redistributions of source code must retain the above copyright notice, this
6
+ list of conditions and the following disclaimer.
7
+ * Redistributions in binary form must reproduce the above copyright notice,
8
+ this list of conditions and the following disclaimer in the documentation
9
+ and/or other materials provided with the distribution.
10
+ * Neither the name of Stefano Balocco nor the names of its contributors may
11
+ be used to endorse or promote products derived from this software without
12
+ specific prior written permission.
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # @stefanobalocco/honosignedrequests
2
+
3
+ A Hono middleware for HMAC-SHA256 signed requests.
4
+
5
+ ## Overview
6
+
7
+ This library provides server-side session management and request signature validation for Hono applications, along with a client library for making signed requests.
8
+
9
+ ### Authentication Mechanism
10
+
11
+ Each session is associated with a cryptographic token (a random byte array) shared between client and server. Every request is authenticated by computing an HMAC-SHA256 signature using this token as the secret key.
12
+
13
+ The signature is computed over:
14
+ - Session ID
15
+ - Sequence number (monotonically increasing to prevent replay attacks)
16
+ - Timestamp (to limit signature validity window)
17
+ - Request parameters (sorted alphabetically)
18
+
19
+ The server validates the signature using the same token, verifies the timestamp falls within the allowed window, and checks that the sequence number is the expected next value for that session.
20
+
21
+ ## Features
22
+ - HMAC-SHA256 request signing with shared secret token
23
+ - Replay attack protection via monotonic sequence numbers
24
+ - Timestamp validation with configurable tolerance, to prevent delayed replay
25
+ - Constant-time signature comparison, to prevent timing attacks
26
+ - Pluggable session storage architecture
27
+ - Works with Node.js, Cloudflare Workers, Deno, and Bun
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install @stefanobalocco/honosignedrequests
33
+ ```
34
+
35
+ ## Server-Side Usage
36
+
37
+ ### Basic Setup
38
+
39
+ ```typescript
40
+ import { Hono } from 'hono';
41
+ import { SignedRequestsManager, SessionsStorageLocal } from '@stefanobalocco/honosignedrequests';
42
+
43
+ const app = new Hono();
44
+
45
+ // SessionsStorageLocal is a simple in-memory storage implementation
46
+ // You can implement your own SessionsStorage (e.g., Redis-based) for production
47
+ const storage = new SessionsStorageLocal({
48
+ maxSessions: 65535, // Storage-specific: maximum concurrent sessions in memory
49
+ maxSessionsPerUser: 3 // Storage-specific: maximum sessions per user
50
+ });
51
+
52
+ // Generic parameters are passed to SignedRequestsManager
53
+ const signedRequests = new SignedRequestsManager(storage, {
54
+ validitySignature: 5000, // signature valid for 5 seconds
55
+ validityToken: 3600000, // session valid for 1 hour
56
+ tokenLength: 32 // token size in bytes
57
+ });
58
+
59
+ app.use('/api/*', signedRequests.middleware);
60
+ ```
61
+
62
+ ### Session Creation (Login Endpoint)
63
+
64
+ ```typescript
65
+ app.post('/auth/login', async (c) => {
66
+ const { username, password } = await c.req.json();
67
+
68
+ // Your authentication logic here
69
+ const userId = await authenticateUser(username, password);
70
+
71
+ if (!userId) {
72
+ return c.json({ error: 'Invalid credentials' }, 401);
73
+ }
74
+
75
+ // Use the manager's createSession method
76
+ const session = await signedRequests.createSession(userId);
77
+
78
+ // Convert token to Base64URL for transmission to client
79
+ const tokenBase64 = btoa(String.fromCharCode(...session.token))
80
+ .replace(/\+/g, '-')
81
+ .replace(/\//g, '_')
82
+ .replace(/=+$/, '');
83
+
84
+ return c.json({
85
+ sessionId: session.id,
86
+ token: tokenBase64,
87
+ sequenceNumber: session.sequenceNumber
88
+ });
89
+ });
90
+ ```
91
+
92
+ ### Ping Endpoint (Verify Authentication)
93
+
94
+ ```typescript
95
+ app.post('/api/ping', (c) => {
96
+ const session = c.get('session');
97
+ return c.json({ pong: !!session });
98
+ });
99
+ ```
100
+
101
+ ### Protected Endpoint
102
+
103
+ ```typescript
104
+ app.post('/api/protected', (c) => {
105
+ const session = c.get('session');
106
+
107
+ if (!session) {
108
+ return c.json({ error: 'Unauthorized' }, 401);
109
+ }
110
+
111
+ return c.json({
112
+ message: 'Authenticated',
113
+ userId: session.userId
114
+ });
115
+ });
116
+ ```
117
+
118
+ ### Configuration
119
+
120
+ The library separates **generic parameters** (common to all storage implementations) from **storage-specific parameters**.
121
+
122
+ #### Generic Parameters (SignedRequestsManagerConfig)
123
+
124
+ These are passed to `SignedRequestsManager` constructor and apply to all storage implementations:
125
+
126
+ | Option | Default | Description |
127
+ |--------|---------|-------------|
128
+ | `validitySignature` | 5000 | Signature validity window in milliseconds |
129
+ | `validityToken` | 3600000 | Session token validity in milliseconds |
130
+ | `tokenLength` | 32 | Token length in bytes (cryptographic secret) |
131
+
132
+ #### SessionsStorageLocal Specific Parameters
133
+
134
+ These are specific to the in-memory implementation:
135
+
136
+ | Option | Default | Description |
137
+ |--------|---------|-------------|
138
+ | `maxSessions` | 65535 | Maximum concurrent sessions in memory |
139
+ | `maxSessionsPerUser` | 3 | Maximum sessions per user (enforced by removing oldest) |
140
+
141
+ ## Client-Side Usage
142
+
143
+ ### Browser Import (CDN)
144
+
145
+ ```html
146
+ <script type="module">
147
+ import { SignedRequester } from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/HonoSignedRequests/client/dist/SignedRequester.min.js';
148
+
149
+ const requester = new SignedRequester();
150
+ // You can also specify a base URL for request
151
+ //const requester = new SignedRequester('https://api.example.com');
152
+
153
+ // Check if we have session data stored
154
+ let needLogin = true;
155
+ if (requester.getSession()) {
156
+ // Try to verify the session is still valid
157
+ try {
158
+ const response = await requester.signedRequestJson('/api/ping', {});
159
+
160
+ if (response?.pong) {
161
+ needLogin = false;
162
+ }
163
+ } catch (error) {
164
+ }
165
+ }
166
+
167
+ if( needLogin ) {
168
+ // No session data, need to login
169
+ const response = await fetch('/auth/login', {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({
173
+ username: 'user@example.com',
174
+ password: 'password123'
175
+ })
176
+ });
177
+
178
+ const loginData = await response.json();
179
+
180
+ // Store session credentials
181
+ requester.setSession({
182
+ sessionId: loginData.sessionId,
183
+ token: loginData.token,
184
+ sequenceNumber: loginData.sequenceNumber
185
+ });
186
+ }
187
+
188
+ // Now we're authenticated, make protected requests
189
+ const data = await requester.signedRequestJson('/api/protected', {
190
+ action: 'getData'
191
+ });
192
+ </script>
193
+ ```
194
+
195
+ ### Client API
196
+
197
+ #### `setSession(config)`
198
+
199
+ Initialize session after login.
200
+
201
+ ```javascript
202
+ requester.setSession({
203
+ sessionId: 12345,
204
+ token: 'base64url_encoded_token',
205
+ sequenceNumber: 1
206
+ });
207
+ ```
208
+
209
+ #### `getSession()`
210
+
211
+ Check if a valid session exists. Loads from localStorage if not in memory.
212
+
213
+ ```javascript
214
+ if (requester.getSession()) {
215
+ // Session available
216
+ }
217
+ ```
218
+
219
+ #### `signedRequest(path, parameters, options?)`
220
+
221
+ Make a signed request, returns the raw Response object.
222
+
223
+ ```javascript
224
+ const response = await requester.signedRequest('/api/action', {
225
+ param1: 'value1',
226
+ param2: 123
227
+ });
228
+ ```
229
+
230
+ #### `signedRequestJson<T>(path, parameters, options?)`
231
+
232
+ Make a signed request and parse JSON response.
233
+
234
+ ```javascript
235
+ const data = await requester.signedRequestJson('/api/data', {
236
+ query: 'example'
237
+ });
238
+ ```
239
+
240
+ #### `clearSession()`
241
+
242
+ Clear session data (for example after you do a logout).
243
+
244
+ ```javascript
245
+ requester.clearSession();
246
+ ```
247
+
248
+ ### Request Options
249
+
250
+ ```typescript
251
+ {
252
+ baseUrl?: string; // Override base URL for this request
253
+ headers?: Record<string, string>; // Additional headers
254
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // HTTP method (default: POST)
255
+ }
256
+ ```
257
+
258
+ ## Signature Format
259
+
260
+ The signature is computed over the following concatenated string:
261
+
262
+ ```
263
+ sessionId={id};sequenceNumber={seq};timestamp={ts};{sorted_params}
264
+ ```
265
+
266
+ The HMAC-SHA256 signature is computed using the session token as the secret key.
267
+
268
+ Parameters are sorted alphabetically by key. Values are serialized as:
269
+ - Primitives (string, number, boolean) and null: `String(value)`
270
+ - Objects and arrays: `JSON.stringify(value)`
271
+
272
+ ## Implementing Custom Storage
273
+
274
+ To implement your own session storage (e.g., Redis-based), extend the `SessionsStorage` abstract class:
275
+
276
+ ```typescript
277
+ import { SessionsStorage } from '@stefanobalocco/honosignedrequests';
278
+ import { Session } from '@stefanobalocco/honosignedrequests';
279
+
280
+ class RedisSessionsStorage extends SessionsStorage {
281
+ async create(
282
+ validityToken: number,
283
+ tokenLength: number,
284
+ userId: number
285
+ ): Promise<Session> {
286
+ // Implement session creation with Redis
287
+ // Generate token with specified tokenLength
288
+ // Use validityToken for Redis TTL or expiration tracking
289
+ // Implement your own maxSessionsPerUser logic if needed
290
+ }
291
+
292
+ async getBySessionId(sessionId: number): Promise<Session | undefined> {
293
+ // Implement session lookup with Redis
294
+ }
295
+
296
+ async getByUserId(userId: number): Promise<Session[]> {
297
+ // Implement session lookup with Redis
298
+ }
299
+
300
+ async delete(sessionId: number): Promise<void> {
301
+ // Implement session deletion with Redis
302
+ }
303
+ }
304
+ ```
305
+
306
+ ## Considerations
307
+ - Storage should expire sessions
308
+ - Invalid sessions should return 401 and trigger client-side session clearing
309
+ - Client should be forced to serialize requests, otherwise out-of-order sequence number may arise triggering a session cleanup.
310
+
311
+ ## License
312
+
313
+ BSD-3-Clause
@@ -0,0 +1,6 @@
1
+ export type Undefinedable<T> = T | undefined;
2
+ export declare function fromBase64Url(b64url: string): Uint8Array<ArrayBuffer>;
3
+ export declare function randomBytes(bytes: number): Uint8Array<ArrayBuffer>;
4
+ export declare function randomInt(min: number, max: number): number;
5
+ export declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
6
+ export declare function hmacSha256(keyBytes: Uint8Array<ArrayBuffer>, data: string): Promise<Uint8Array<ArrayBuffer>>;
package/dist/Common.js ADDED
@@ -0,0 +1,45 @@
1
+ export function fromBase64Url(b64url) {
2
+ const pad = (4 - (b64url.length % 4)) % 4;
3
+ const b64 = (b64url + "=".repeat(pad)).replace(/-/g, "+").replace(/_/g, "/");
4
+ const binary = atob(b64);
5
+ const cFL = binary.length;
6
+ const returnValue = new Uint8Array(cFL);
7
+ for (let iFL = 0; iFL < cFL; iFL++) {
8
+ returnValue[iFL] = binary.charCodeAt(iFL);
9
+ }
10
+ return returnValue;
11
+ }
12
+ export function randomBytes(bytes) {
13
+ const returnValue = new Uint8Array(bytes);
14
+ crypto.getRandomValues(returnValue);
15
+ return returnValue;
16
+ }
17
+ export function randomInt(min, max) {
18
+ let returnValue;
19
+ const range = max - min;
20
+ if (range > 0) {
21
+ const randomBuffer = new Uint32Array(randomBytes(4).buffer);
22
+ returnValue = min + (randomBuffer[0] % range);
23
+ }
24
+ else {
25
+ throw new Error('max must be > min');
26
+ }
27
+ return returnValue;
28
+ }
29
+ export function constantTimeEqual(a, b) {
30
+ let returnValue = false;
31
+ if (a.length === b.length) {
32
+ let diff = 0;
33
+ for (let i = 0; i < a.length; i++) {
34
+ diff |= a[i] ^ b[i];
35
+ }
36
+ returnValue = (diff === 0);
37
+ }
38
+ return returnValue;
39
+ }
40
+ export async function hmacSha256(keyBytes, data) {
41
+ const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
42
+ const textEncoder = new TextEncoder();
43
+ const signature = await crypto.subtle.sign("HMAC", key, textEncoder.encode(data));
44
+ return new Uint8Array(signature);
45
+ }
@@ -0,0 +1,8 @@
1
+ export type Session = {
2
+ id: number;
3
+ userId: number;
4
+ sequenceNumber: number;
5
+ token: Uint8Array<ArrayBuffer>;
6
+ lastUsed: number;
7
+ data: [string, any][];
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { Undefinedable } from './Common';
2
+ import { Session } from './Session';
3
+ export declare abstract class SessionsStorage {
4
+ abstract create(validityToken: number, tokenLength: number, userId: number): Promise<Session>;
5
+ abstract getBySessionId(sessionId: number): Promise<Undefinedable<Session>>;
6
+ abstract getByUserId(userId: number): Promise<Session[]>;
7
+ abstract delete(sessionId: number): Promise<boolean>;
8
+ }
@@ -0,0 +1,2 @@
1
+ export class SessionsStorage {
2
+ }
@@ -0,0 +1,20 @@
1
+ import { Undefinedable } from './Common';
2
+ import { Session } from './Session';
3
+ import { SessionsStorage } from './SessionsStorage';
4
+ type SessionStorageLocalConfig = {
5
+ maxSessions: number;
6
+ maxSessionsPerUser: number;
7
+ };
8
+ export declare class SessionsStorageLocal implements SessionsStorage {
9
+ private readonly _maxSessions;
10
+ private readonly _maxSessionsPerUser;
11
+ private readonly _cleanupSessionLimit;
12
+ private _sessionsById;
13
+ private _sessionsByUserId;
14
+ constructor(options?: Partial<SessionStorageLocalConfig>);
15
+ create(validityToken: number, tokenLength: number, userId: number): Promise<Session>;
16
+ delete(sessionId: number): Promise<boolean>;
17
+ getByUserId(userId: number): Promise<Session[]>;
18
+ getBySessionId(sessionId: number): Promise<Undefinedable<Session>>;
19
+ }
20
+ export {};
@@ -0,0 +1,88 @@
1
+ import { randomBytes, randomInt } from './Common';
2
+ export class SessionsStorageLocal {
3
+ _maxSessions;
4
+ _maxSessionsPerUser;
5
+ _cleanupSessionLimit;
6
+ _sessionsById = new Map();
7
+ _sessionsByUserId = new Map();
8
+ constructor(options) {
9
+ this._maxSessions = options?.maxSessions ?? 0xFFFF;
10
+ this._maxSessionsPerUser = options?.maxSessionsPerUser ?? 3;
11
+ this._cleanupSessionLimit = Math.floor(this._maxSessions * 0.75);
12
+ }
13
+ async create(validityToken, tokenLength, userId) {
14
+ let returnValue;
15
+ const now = Date.now();
16
+ if (this._sessionsById.size > this._cleanupSessionLimit) {
17
+ await Promise.all(Array.from(this._sessionsById.entries()).filter(([_, session]) => now > (session.lastUsed + validityToken)).map(([sessionId, _]) => this.delete(sessionId)));
18
+ }
19
+ const usedIds = [...this._sessionsById.keys()].filter((sessionId) => (now <= (this._sessionsById.get(sessionId).lastUsed + validityToken))).sort((a, b) => a - b);
20
+ const sessionsRange = this._maxSessions - usedIds.length;
21
+ if (sessionsRange > 0) {
22
+ let sessionId = randomInt(0, this._maxSessions - usedIds.length);
23
+ let left = 0;
24
+ let right = usedIds.length;
25
+ while (left < right) {
26
+ const mid = Math.floor((left + right) / 2);
27
+ if (usedIds[mid] <= sessionId + mid) {
28
+ left = mid + 1;
29
+ }
30
+ else {
31
+ right = mid;
32
+ }
33
+ }
34
+ sessionId = (sessionId + left) >>> 0;
35
+ const session = this._sessionsById.get(sessionId);
36
+ if (session) {
37
+ if (now > session.lastUsed + validityToken) {
38
+ await this.delete(sessionId);
39
+ }
40
+ else {
41
+ throw new Error(`Session ${sessionId} already in use`);
42
+ }
43
+ }
44
+ const token = randomBytes(tokenLength);
45
+ returnValue = {
46
+ id: sessionId,
47
+ userId,
48
+ sequenceNumber: 1,
49
+ token,
50
+ lastUsed: now,
51
+ data: []
52
+ };
53
+ this._sessionsById.set(sessionId, returnValue);
54
+ const sessionsByUserId = this._sessionsByUserId.get(userId) ?? [];
55
+ sessionsByUserId.push(returnValue);
56
+ if (sessionsByUserId.length > this._maxSessionsPerUser) {
57
+ const oldestIndex = sessionsByUserId.reduce((minimumIndex, session, index) => session.lastUsed < sessionsByUserId[minimumIndex].lastUsed ? index : minimumIndex, 0);
58
+ const old = sessionsByUserId.splice(oldestIndex, 1)[0];
59
+ this._sessionsById.delete(old.id);
60
+ }
61
+ this._sessionsByUserId.set(userId, sessionsByUserId);
62
+ }
63
+ else {
64
+ throw new Error(`Session array full`);
65
+ }
66
+ return returnValue;
67
+ }
68
+ async delete(sessionId) {
69
+ let returnValue = false;
70
+ const session = this._sessionsById.get(sessionId);
71
+ if (session) {
72
+ returnValue = true;
73
+ this._sessionsById.delete(sessionId);
74
+ const userSessions = this._sessionsByUserId.get(session.userId) ?? [];
75
+ const sessionIndex = userSessions.findIndex((session) => session.id === sessionId);
76
+ if (-1 !== sessionIndex) {
77
+ userSessions.splice(sessionIndex, 1);
78
+ }
79
+ }
80
+ return returnValue;
81
+ }
82
+ async getByUserId(userId) {
83
+ return this._sessionsByUserId.get(userId) ?? [];
84
+ }
85
+ async getBySessionId(sessionId) {
86
+ return this._sessionsById.get(sessionId);
87
+ }
88
+ }
@@ -0,0 +1,21 @@
1
+ import { MiddlewareHandler } from 'hono';
2
+ import { Undefinedable } from './Common';
3
+ import { Session } from './Session';
4
+ import { SessionsStorage } from './SessionsStorage';
5
+ type SignedRequestsManagerConfig = {
6
+ validitySignature: number;
7
+ validityToken: number;
8
+ tokenLength: number;
9
+ };
10
+ export declare class SignedRequestsManager {
11
+ private static readonly _primitives;
12
+ private readonly _storage;
13
+ private readonly _validitySignature;
14
+ private readonly _validityToken;
15
+ private readonly _tokenLength;
16
+ constructor(storage?: SessionsStorage, options?: Partial<SignedRequestsManagerConfig>);
17
+ createSession(userId: number): Promise<Session>;
18
+ validate(sessionId: number, timestamp: number, parameters: [string, any][], signature: Uint8Array<ArrayBuffer>): Promise<Undefinedable<Session>>;
19
+ middleware: MiddlewareHandler;
20
+ }
21
+ export {};
@@ -0,0 +1,91 @@
1
+ import { constantTimeEqual, fromBase64Url, hmacSha256 } from './Common';
2
+ import { SessionsStorageLocal } from './SessionsStorageLocal';
3
+ export class SignedRequestsManager {
4
+ static _primitives = new Set(['string', 'number', 'boolean']);
5
+ _storage;
6
+ _validitySignature;
7
+ _validityToken;
8
+ _tokenLength;
9
+ constructor(storage, options) {
10
+ this._validitySignature = options?.validitySignature ?? 5000;
11
+ this._validityToken = options?.validityToken ?? 60 * 60000;
12
+ this._tokenLength = options?.tokenLength ?? 32;
13
+ if (!storage) {
14
+ storage = new SessionsStorageLocal();
15
+ }
16
+ this._storage = storage;
17
+ }
18
+ async createSession(userId) {
19
+ return await this._storage.create(this._validityToken, this._tokenLength, userId);
20
+ }
21
+ async validate(sessionId, timestamp, parameters, signature) {
22
+ let returnValue;
23
+ const now = Date.now();
24
+ if ((now > timestamp) && (now < timestamp + this._validitySignature)) {
25
+ const session = await this._storage.getBySessionId(sessionId);
26
+ if (session) {
27
+ if (now < session.lastUsed + this._validityToken) {
28
+ const parametersOrdered = [
29
+ ['sessionId', session.id],
30
+ ['sequenceNumber', session.sequenceNumber],
31
+ ['timestamp', timestamp],
32
+ ...parameters.sort((a, b) => a[0].localeCompare(b[0]))
33
+ ];
34
+ const dataToSign = parametersOrdered.map(([name, value]) => {
35
+ const serializedValue = (SignedRequestsManager._primitives.has(typeof value) || null === value) ? String(value) : JSON.stringify(value);
36
+ return `${name}=${serializedValue}`;
37
+ }).join(';');
38
+ const signatureExpected = await hmacSha256(session.token, dataToSign);
39
+ if (constantTimeEqual(signature, signatureExpected)) {
40
+ session.lastUsed = now;
41
+ session.sequenceNumber++;
42
+ returnValue = session;
43
+ }
44
+ }
45
+ else {
46
+ await this._storage.delete(sessionId);
47
+ }
48
+ }
49
+ }
50
+ return returnValue;
51
+ }
52
+ middleware = async (context, next) => {
53
+ let session;
54
+ try {
55
+ const parameters = {};
56
+ switch (context.req.method) {
57
+ case 'GET': {
58
+ Object.assign(parameters, context.req.query());
59
+ break;
60
+ }
61
+ case 'POST': {
62
+ switch (context.req.header('Content-Type')) {
63
+ case 'application/json': {
64
+ Object.assign(parameters, await context.req.json());
65
+ break;
66
+ }
67
+ default: {
68
+ Object.assign(parameters, await context.req.parseBody());
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ const sessionId = parseInt(parameters.sessionId, 10);
75
+ const timestamp = parseInt(parameters.timestamp, 10);
76
+ const signature = fromBase64Url(parameters.signature);
77
+ if (sessionId && timestamp && signature) {
78
+ const { sessionId: _, timestamp: __, signature: ___, ...other } = parameters;
79
+ const otherParameters = Object.entries(other);
80
+ session = await this.validate(sessionId, timestamp, otherParameters, signature);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error('Session validation error:', error);
85
+ }
86
+ if (session) {
87
+ context.set('session', session);
88
+ }
89
+ await next();
90
+ };
91
+ }
@@ -0,0 +1,4 @@
1
+ export type { Session } from './Session.js';
2
+ export type { SessionsStorage } from './SessionsStorage.js';
3
+ export { SessionsStorageLocal } from './SessionsStorageLocal.js';
4
+ export { SignedRequestsManager } from './SignedRequestsManager.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { SessionsStorageLocal } from './SessionsStorageLocal.js';
2
+ export { SignedRequestsManager } from './SignedRequestsManager.js';
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@stefanobalocco/honosignedrequests",
3
+ "version": "1.0.0",
4
+ "description": "An hono middleware to manage signed requests, including a client implementation.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./client": {
13
+ "types": "./client/dist/SignedRequester.d.ts",
14
+ "import": "./client/dist/SignedRequester.min.js"
15
+ }
16
+ },
17
+ "devDependencies": {
18
+ "hono": "^4.10.6",
19
+ "terser": "^5.44.1",
20
+ "typescript": "^5.5.3"
21
+ },
22
+ "peerDependencies": {
23
+ "hono": "^4.0.0"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "keywords": [
29
+ "hono",
30
+ "middleware",
31
+ "hmac",
32
+ "signed-requests",
33
+ "session",
34
+ "authentication"
35
+ ],
36
+ "license": "BSD-3-Clause",
37
+ "scripts": {
38
+ "build": "npm run build:server && npm run build:client",
39
+ "build:server": "tsc",
40
+ "build:client": "tsc -p client/tsconfig.json && terser client/dist/SignedRequester.js -o client/dist/SignedRequester.min.js --toplevel -m -c --mangle-props regex=/^_/"
41
+ }
42
+ }