@stefanobalocco/honosignedrequests 1.0.0 → 1.1.2
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 +1 -1
- package/README.md +15 -1
- package/client/dist/SignedRequester.d.ts +38 -0
- package/client/dist/SignedRequester.js +219 -0
- package/client/dist/SignedRequester.min.js +1 -0
- package/dist/Common.d.ts +2 -0
- package/dist/Common.js +3 -1
- package/dist/SessionsStorage.d.ts +2 -2
- package/dist/SessionsStorageLocal.d.ts +3 -3
- package/dist/SessionsStorageLocal.js +1 -1
- package/dist/SignedRequestsManager.d.ts +5 -3
- package/dist/SignedRequestsManager.js +20 -10
- package/package.json +29 -11
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2025, Stefano Balocco <stefano.balocco@gmail.com>
|
|
1
|
+
Copyright (c) 2025-2026, Stefano Balocco <stefano.balocco@gmail.com>
|
|
2
2
|
All rights reserved.
|
|
3
3
|
Redistribution and use in source and binary forms, with or without
|
|
4
4
|
modification, are permitted provided that the following conditions are met:
|
package/README.md
CHANGED
|
@@ -128,6 +128,7 @@ These are passed to `SignedRequestsManager` constructor and apply to all storage
|
|
|
128
128
|
| `validitySignature` | 5000 | Signature validity window in milliseconds |
|
|
129
129
|
| `validityToken` | 3600000 | Session token validity in milliseconds |
|
|
130
130
|
| `tokenLength` | 32 | Token length in bytes (cryptographic secret) |
|
|
131
|
+
| `onError` | undefined | Callback invoked when an error occurs during request validation |
|
|
131
132
|
|
|
132
133
|
#### SessionsStorageLocal Specific Parameters
|
|
133
134
|
|
|
@@ -147,8 +148,10 @@ These are specific to the in-memory implementation:
|
|
|
147
148
|
import { SignedRequester } from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/HonoSignedRequests/client/dist/SignedRequester.min.js';
|
|
148
149
|
|
|
149
150
|
const requester = new SignedRequester();
|
|
150
|
-
// You can also specify a base URL for
|
|
151
|
+
// You can also specify a base URL for requests
|
|
151
152
|
//const requester = new SignedRequester('https://api.example.com');
|
|
153
|
+
// You can also specify an error handler for encoding/decoding errors
|
|
154
|
+
//const requester = new SignedRequester('https://api.example.com', (error) => console.error(error));
|
|
152
155
|
|
|
153
156
|
// Check if we have session data stored
|
|
154
157
|
let needLogin = true;
|
|
@@ -194,6 +197,17 @@ These are specific to the in-memory implementation:
|
|
|
194
197
|
|
|
195
198
|
### Client API
|
|
196
199
|
|
|
200
|
+
#### Constructor
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
const requester = new SignedRequester(baseUrl?, onError?);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Parameter | Type | Description |
|
|
207
|
+
|-----------|------|-------------|
|
|
208
|
+
| `baseUrl` | `string` | Optional base URL for all requests. If not provided, relative paths are used. |
|
|
209
|
+
| `onError` | `(error: unknown) => void` | Optional callback invoked when encoding/decoding errors occur. |
|
|
210
|
+
|
|
197
211
|
#### `setSession(config)`
|
|
198
212
|
|
|
199
213
|
Initialize session after login.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
2
|
+
type SessionConfig = {
|
|
3
|
+
sessionId: number;
|
|
4
|
+
token: string;
|
|
5
|
+
sequenceNumber: number;
|
|
6
|
+
};
|
|
7
|
+
type SignedRequest<T = Record<string, any>> = {
|
|
8
|
+
sessionId: number;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
signature: string;
|
|
11
|
+
} & T;
|
|
12
|
+
type SignedRequestOptions = {
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
method?: Methods;
|
|
16
|
+
};
|
|
17
|
+
declare class SignedRequester {
|
|
18
|
+
private static readonly _primitives;
|
|
19
|
+
private readonly _baseUrl;
|
|
20
|
+
private readonly _onError?;
|
|
21
|
+
private _sessionId;
|
|
22
|
+
private _token;
|
|
23
|
+
private _sequenceNumber;
|
|
24
|
+
private _semaphore;
|
|
25
|
+
private _semaphoreQueue;
|
|
26
|
+
private _semaphoreAcquire;
|
|
27
|
+
private _semaphoreRelease;
|
|
28
|
+
private _incrementSequenceNumber;
|
|
29
|
+
private _loadFromStorage;
|
|
30
|
+
constructor(baseUrl?: string, onError?: (error: unknown) => void);
|
|
31
|
+
setSession(config: SessionConfig): void;
|
|
32
|
+
getSession(): boolean;
|
|
33
|
+
clearSession(): void;
|
|
34
|
+
signedRequest(path: string, parameters: Record<string, any>, options?: SignedRequestOptions): Promise<Response>;
|
|
35
|
+
signedRequestJson<T = any>(path: string, parameters: Record<string, any>, options?: SignedRequestOptions): Promise<T>;
|
|
36
|
+
}
|
|
37
|
+
export type { SessionConfig, SignedRequest, SignedRequestOptions };
|
|
38
|
+
export { SignedRequester };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
function _base64url_encode(value, onError) {
|
|
2
|
+
let returnValue = '';
|
|
3
|
+
if (0 < value?.length) {
|
|
4
|
+
try {
|
|
5
|
+
const base64String = Array.from(value, (byte) => String.fromCharCode(byte)).join('');
|
|
6
|
+
returnValue = btoa(base64String)
|
|
7
|
+
.replace(/\+/g, '-')
|
|
8
|
+
.replace(/\//g, '_')
|
|
9
|
+
.replace(/=+$/, '');
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
onError?.(error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return returnValue;
|
|
16
|
+
}
|
|
17
|
+
function _base64url_decode(value, onError) {
|
|
18
|
+
let returnValue;
|
|
19
|
+
if (0 < value?.length && _base64UrlVerify.test(value)) {
|
|
20
|
+
const padding = value.length % 4;
|
|
21
|
+
const paddedValue = 0 === padding ? value : value.padEnd(value.length + (4 - padding), '=');
|
|
22
|
+
const base64 = paddedValue.replace(/-/g, '+').replace(/_/g, '/');
|
|
23
|
+
try {
|
|
24
|
+
const binaryString = atob(base64);
|
|
25
|
+
returnValue = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
onError?.(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return returnValue;
|
|
32
|
+
}
|
|
33
|
+
const _base64UrlVerify = /^(?=.)(?:[A-Za-z0-9\-_]{4})*(?:[A-Za-z0-9\-_]{2}(?:==)?|[A-Za-z0-9\-_]{3}=?)?$/;
|
|
34
|
+
class SignedRequester {
|
|
35
|
+
_semaphoreAcquire(wait = true) {
|
|
36
|
+
let returnValue = Promise.resolve(false);
|
|
37
|
+
if (!this._semaphore) {
|
|
38
|
+
this._semaphore = true;
|
|
39
|
+
returnValue = Promise.resolve(true);
|
|
40
|
+
}
|
|
41
|
+
else if (wait) {
|
|
42
|
+
returnValue = new Promise((resolve) => {
|
|
43
|
+
this._semaphoreQueue.push(resolve);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return returnValue;
|
|
47
|
+
}
|
|
48
|
+
_semaphoreRelease() {
|
|
49
|
+
if (this._semaphore) {
|
|
50
|
+
if (0 < this._semaphoreQueue.length) {
|
|
51
|
+
const nextWaiting = this._semaphoreQueue.shift();
|
|
52
|
+
if (nextWaiting) {
|
|
53
|
+
nextWaiting(true);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this._semaphore = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
_incrementSequenceNumber() {
|
|
62
|
+
if (undefined !== this._sequenceNumber) {
|
|
63
|
+
this._sequenceNumber++;
|
|
64
|
+
localStorage.setItem('sequenceNumber', this._sequenceNumber.toString());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
_loadFromStorage() {
|
|
68
|
+
let returnValue = false;
|
|
69
|
+
const sessionIdStr = localStorage.getItem('sessionId');
|
|
70
|
+
const tokenStr = localStorage.getItem('token');
|
|
71
|
+
const sequenceNumberStr = localStorage.getItem('sequenceNumber');
|
|
72
|
+
if (sessionIdStr && tokenStr && sequenceNumberStr) {
|
|
73
|
+
const sessionId = parseInt(sessionIdStr);
|
|
74
|
+
const sequenceNumber = parseInt(sequenceNumberStr);
|
|
75
|
+
const token = _base64url_decode(tokenStr, this._onError);
|
|
76
|
+
if (!isNaN(sessionId) && !isNaN(sequenceNumber) && token) {
|
|
77
|
+
this._sessionId = sessionId;
|
|
78
|
+
this._token = token;
|
|
79
|
+
this._sequenceNumber = sequenceNumber;
|
|
80
|
+
returnValue = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return returnValue;
|
|
84
|
+
}
|
|
85
|
+
constructor(baseUrl, onError) {
|
|
86
|
+
this._semaphore = false;
|
|
87
|
+
this._semaphoreQueue = [];
|
|
88
|
+
this._baseUrl = baseUrl;
|
|
89
|
+
this._onError = onError;
|
|
90
|
+
}
|
|
91
|
+
setSession(config) {
|
|
92
|
+
const token = _base64url_decode(config.token, this._onError);
|
|
93
|
+
if (token) {
|
|
94
|
+
this._sessionId = config.sessionId;
|
|
95
|
+
this._token = token;
|
|
96
|
+
this._sequenceNumber = config.sequenceNumber;
|
|
97
|
+
localStorage.setItem('sessionId', config.sessionId.toString());
|
|
98
|
+
localStorage.setItem('token', config.token);
|
|
99
|
+
localStorage.setItem('sequenceNumber', config.sequenceNumber.toString());
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
throw new Error('Invalid token format');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
getSession() {
|
|
106
|
+
let returnValue;
|
|
107
|
+
if (undefined !== this._sessionId &&
|
|
108
|
+
undefined !== this._token &&
|
|
109
|
+
undefined !== this._sequenceNumber) {
|
|
110
|
+
returnValue = true;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
returnValue = this._loadFromStorage();
|
|
114
|
+
}
|
|
115
|
+
return returnValue;
|
|
116
|
+
}
|
|
117
|
+
clearSession() {
|
|
118
|
+
localStorage.removeItem('sessionId');
|
|
119
|
+
localStorage.removeItem('token');
|
|
120
|
+
localStorage.removeItem('sequenceNumber');
|
|
121
|
+
this._sessionId = undefined;
|
|
122
|
+
this._token = undefined;
|
|
123
|
+
this._sequenceNumber = undefined;
|
|
124
|
+
}
|
|
125
|
+
async signedRequest(path, parameters, options = {}) {
|
|
126
|
+
let returnValue;
|
|
127
|
+
let error;
|
|
128
|
+
await this._semaphoreAcquire();
|
|
129
|
+
try {
|
|
130
|
+
if (undefined !== this._sessionId &&
|
|
131
|
+
undefined !== this._token &&
|
|
132
|
+
undefined !== this._sequenceNumber) {
|
|
133
|
+
const timestamp = Date.now();
|
|
134
|
+
const parametersArray = Object.entries(parameters);
|
|
135
|
+
const parametersOrdered = [
|
|
136
|
+
['sessionId', this._sessionId],
|
|
137
|
+
['sequenceNumber', this._sequenceNumber],
|
|
138
|
+
['timestamp', timestamp],
|
|
139
|
+
...parametersArray.sort((a, b) => a[0].localeCompare(b[0]))
|
|
140
|
+
];
|
|
141
|
+
const dataToSign = parametersOrdered
|
|
142
|
+
.map(([name, value]) => {
|
|
143
|
+
const serializedValue = SignedRequester._primitives.has(typeof value) || null === value
|
|
144
|
+
? String(value)
|
|
145
|
+
: JSON.stringify(value);
|
|
146
|
+
return `${name}=${serializedValue}`;
|
|
147
|
+
})
|
|
148
|
+
.join(';');
|
|
149
|
+
const cryptoKey = await crypto.subtle.importKey('raw', this._token, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
150
|
+
const encoder = new TextEncoder();
|
|
151
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(dataToSign));
|
|
152
|
+
const signature = _base64url_encode(new Uint8Array(signatureBuffer), this._onError);
|
|
153
|
+
const signedPayload = {
|
|
154
|
+
sessionId: this._sessionId,
|
|
155
|
+
timestamp: timestamp,
|
|
156
|
+
signature: signature
|
|
157
|
+
};
|
|
158
|
+
Object.assign(signedPayload, Object.fromEntries(parametersArray));
|
|
159
|
+
const url = options.baseUrl
|
|
160
|
+
? `${options.baseUrl}${path}`
|
|
161
|
+
: (this._baseUrl ? `${this._baseUrl}${path}` : path);
|
|
162
|
+
const method = options.method || 'POST';
|
|
163
|
+
returnValue = await fetch(url, {
|
|
164
|
+
method: method,
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
...options.headers
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify(signedPayload)
|
|
170
|
+
});
|
|
171
|
+
if (returnValue.ok) {
|
|
172
|
+
this._incrementSequenceNumber();
|
|
173
|
+
}
|
|
174
|
+
else if (401 === returnValue.status) {
|
|
175
|
+
this.clearSession();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
error = new Error('Session not configured');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
this._semaphoreRelease();
|
|
187
|
+
}
|
|
188
|
+
if (error) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
return returnValue;
|
|
192
|
+
}
|
|
193
|
+
async signedRequestJson(path, parameters, options = {}) {
|
|
194
|
+
let returnValue;
|
|
195
|
+
let error;
|
|
196
|
+
try {
|
|
197
|
+
const response = await this.signedRequest(path, parameters, options);
|
|
198
|
+
if (response.ok) {
|
|
199
|
+
returnValue = (await response.json());
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
error = new Error(`Request failed with status ${response.status}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
207
|
+
}
|
|
208
|
+
if (error) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
return returnValue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
SignedRequester._primitives = new Set([
|
|
215
|
+
'undefined',
|
|
216
|
+
'string',
|
|
217
|
+
'number'
|
|
218
|
+
]);
|
|
219
|
+
export { SignedRequester };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function t(t,s){let i;if(0<t?.length&&e.test(t)){const e=t.length%4,o=(0===e?t:t.padEnd(t.length+(4-e),"=")).replace(/-/g,"+").replace(/_/g,"/");try{const t=atob(o);i=Uint8Array.from(t,t=>t.charCodeAt(0))}catch(t){s?.(t)}}return i}const e=/^(?=.)(?:[A-Za-z0-9\-_]{4})*(?:[A-Za-z0-9\-_]{2}(?:==)?|[A-Za-z0-9\-_]{3}=?)?$/;class s{t(t=!0){let e=Promise.resolve(!1);return this.i?t&&(e=new Promise(t=>{this.o.push(t)})):(this.i=!0,e=Promise.resolve(!0)),e}h(){if(this.i)if(0<this.o.length){const t=this.o.shift();t&&t(!0)}else this.i=!1}l(){void 0!==this.u&&(this.u++,localStorage.setItem("sequenceNumber",this.u.toString()))}S(){let e=!1;const s=localStorage.getItem("sessionId"),i=localStorage.getItem("token"),o=localStorage.getItem("sequenceNumber");if(s&&i&&o){const r=parseInt(s),n=parseInt(o),a=t(i,this.m);isNaN(r)||isNaN(n)||!a||(this.p=r,this.N=a,this.u=n,e=!0)}return e}constructor(t,e){this.i=!1,this.o=[],this.v=t,this.m=e}setSession(e){const s=t(e.token,this.m);if(!s)throw new Error("Invalid token format");this.p=e.sessionId,this.N=s,this.u=e.sequenceNumber,localStorage.setItem("sessionId",e.sessionId.toString()),localStorage.setItem("token",e.token),localStorage.setItem("sequenceNumber",e.sequenceNumber.toString())}getSession(){let t;return t=void 0!==this.p&&void 0!==this.N&&void 0!==this.u||this.S(),t}clearSession(){localStorage.removeItem("sessionId"),localStorage.removeItem("token"),localStorage.removeItem("sequenceNumber"),this.p=void 0,this.N=void 0,this.u=void 0}async signedRequest(t,e,i={}){let o,r;await this.t();try{if(void 0!==this.p&&void 0!==this.N&&void 0!==this.u){const r=Date.now(),n=Object.entries(e),a=[["sessionId",this.p],["sequenceNumber",this.u],["timestamp",r],...n.sort((t,e)=>t[0].localeCompare(e[0]))].map(([t,e])=>`${t}=${s.q.has(typeof e)||null===e?String(e):JSON.stringify(e)}`).join(";"),h=await crypto.subtle.importKey("raw",this.N,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),c=new TextEncoder,l=await crypto.subtle.sign("HMAC",h,c.encode(a)),u=function(t,e){let s="";if(0<t?.length)try{const e=Array.from(t,t=>String.fromCharCode(t)).join("");s=btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}catch(t){e?.(t)}return s}(new Uint8Array(l),this.m),d={sessionId:this.p,timestamp:r,signature:u};Object.assign(d,Object.fromEntries(n));const g=i.baseUrl?`${i.baseUrl}${t}`:this.v?`${this.v}${t}`:t,S=i.method||"POST";o=await fetch(g,{method:S,headers:{"Content-Type":"application/json",...i.headers},body:JSON.stringify(d)}),o.ok?this.l():401===o.status&&this.clearSession()}else r=new Error("Session not configured")}catch(t){r=t instanceof Error?t:new Error(String(t))}finally{this.h()}if(r)throw r;return o}async signedRequestJson(t,e,s={}){let i,o;try{const r=await this.signedRequest(t,e,s);r.ok?i=await r.json():o=new Error(`Request failed with status ${r.status}`)}catch(t){o=t instanceof Error?t:new Error(String(t))}if(o)throw o;return i}}s.q=new Set(["undefined","string","number"]);export{s as SignedRequester};
|
package/dist/Common.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export declare function randomBytes(bytes: number): Uint8Array<ArrayBuffer>;
|
|
|
4
4
|
export declare function randomInt(min: number, max: number): number;
|
|
5
5
|
export declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
6
6
|
export declare function hmacSha256(keyBytes: Uint8Array<ArrayBuffer>, data: string): Promise<Uint8Array<ArrayBuffer>>;
|
|
7
|
+
export declare const base64Verify: RegExp;
|
|
8
|
+
export declare const base64UrlVerify: RegExp;
|
package/dist/Common.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function fromBase64Url(b64url) {
|
|
2
|
-
const pad = (4 - (b64url
|
|
2
|
+
const pad = (4 - (b64url?.length % 4)) % 4;
|
|
3
3
|
const b64 = (b64url + "=".repeat(pad)).replace(/-/g, "+").replace(/_/g, "/");
|
|
4
4
|
const binary = atob(b64);
|
|
5
5
|
const cFL = binary.length;
|
|
@@ -43,3 +43,5 @@ export async function hmacSha256(keyBytes, data) {
|
|
|
43
43
|
const signature = await crypto.subtle.sign("HMAC", key, textEncoder.encode(data));
|
|
44
44
|
return new Uint8Array(signature);
|
|
45
45
|
}
|
|
46
|
+
export const base64Verify = /^(?=.)(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
47
|
+
export const base64UrlVerify = /^(?=.)(?:[A-Za-z0-9\-_]{4})*(?:[A-Za-z0-9\-_]{2}(?:==)?|[A-Za-z0-9\-_]{3}=?)?$/;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Undefinedable } from './Common';
|
|
2
|
-
import { Session } from './Session';
|
|
1
|
+
import { Undefinedable } from './Common.js';
|
|
2
|
+
import { Session } from './Session.js';
|
|
3
3
|
export declare abstract class SessionsStorage {
|
|
4
4
|
abstract create(validityToken: number, tokenLength: number, userId: number): Promise<Session>;
|
|
5
5
|
abstract getBySessionId(sessionId: number): Promise<Undefinedable<Session>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Undefinedable } from './Common';
|
|
2
|
-
import { Session } from './Session';
|
|
3
|
-
import { SessionsStorage } from './SessionsStorage';
|
|
1
|
+
import { Undefinedable } from './Common.js';
|
|
2
|
+
import { Session } from './Session.js';
|
|
3
|
+
import { SessionsStorage } from './SessionsStorage.js';
|
|
4
4
|
type SessionStorageLocalConfig = {
|
|
5
5
|
maxSessions: number;
|
|
6
6
|
maxSessionsPerUser: number;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { MiddlewareHandler } from 'hono';
|
|
2
|
-
import { Undefinedable } from './Common';
|
|
3
|
-
import { Session } from './Session';
|
|
4
|
-
import { SessionsStorage } from './SessionsStorage';
|
|
2
|
+
import { Undefinedable } from './Common.js';
|
|
3
|
+
import { Session } from './Session.js';
|
|
4
|
+
import { SessionsStorage } from './SessionsStorage.js';
|
|
5
5
|
type SignedRequestsManagerConfig = {
|
|
6
6
|
validitySignature: number;
|
|
7
7
|
validityToken: number;
|
|
8
8
|
tokenLength: number;
|
|
9
|
+
onError?: (error: unknown) => void;
|
|
9
10
|
};
|
|
10
11
|
export declare class SignedRequestsManager {
|
|
11
12
|
private static readonly _primitives;
|
|
@@ -13,6 +14,7 @@ export declare class SignedRequestsManager {
|
|
|
13
14
|
private readonly _validitySignature;
|
|
14
15
|
private readonly _validityToken;
|
|
15
16
|
private readonly _tokenLength;
|
|
17
|
+
private readonly _onError?;
|
|
16
18
|
constructor(storage?: SessionsStorage, options?: Partial<SignedRequestsManagerConfig>);
|
|
17
19
|
createSession(userId: number): Promise<Session>;
|
|
18
20
|
validate(sessionId: number, timestamp: number, parameters: [string, any][], signature: Uint8Array<ArrayBuffer>): Promise<Undefinedable<Session>>;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { constantTimeEqual, fromBase64Url, hmacSha256 } from './Common';
|
|
2
|
-
import { SessionsStorageLocal } from './SessionsStorageLocal';
|
|
1
|
+
import { base64UrlVerify, constantTimeEqual, fromBase64Url, hmacSha256 } from './Common.js';
|
|
2
|
+
import { SessionsStorageLocal } from './SessionsStorageLocal.js';
|
|
3
3
|
export class SignedRequestsManager {
|
|
4
4
|
static _primitives = new Set(['string', 'number', 'boolean']);
|
|
5
5
|
_storage;
|
|
6
6
|
_validitySignature;
|
|
7
7
|
_validityToken;
|
|
8
8
|
_tokenLength;
|
|
9
|
+
_onError;
|
|
9
10
|
constructor(storage, options) {
|
|
10
11
|
this._validitySignature = options?.validitySignature ?? 5000;
|
|
11
12
|
this._validityToken = options?.validityToken ?? 60 * 60000;
|
|
12
13
|
this._tokenLength = options?.tokenLength ?? 32;
|
|
14
|
+
this._onError = options?.onError;
|
|
13
15
|
if (!storage) {
|
|
14
16
|
storage = new SessionsStorageLocal();
|
|
15
17
|
}
|
|
@@ -59,12 +61,13 @@ export class SignedRequestsManager {
|
|
|
59
61
|
break;
|
|
60
62
|
}
|
|
61
63
|
case 'POST': {
|
|
62
|
-
switch (context.req.header('Content-Type')) {
|
|
64
|
+
switch (context.req.header('Content-Type')?.split(';')[0].trim().toLowerCase()) {
|
|
63
65
|
case 'application/json': {
|
|
64
66
|
Object.assign(parameters, await context.req.json());
|
|
65
67
|
break;
|
|
66
68
|
}
|
|
67
|
-
|
|
69
|
+
case 'multipart/form-data':
|
|
70
|
+
case 'application/x-www-form-urlencoded': {
|
|
68
71
|
Object.assign(parameters, await context.req.parseBody());
|
|
69
72
|
break;
|
|
70
73
|
}
|
|
@@ -73,15 +76,22 @@ export class SignedRequestsManager {
|
|
|
73
76
|
}
|
|
74
77
|
const sessionId = parseInt(parameters.sessionId, 10);
|
|
75
78
|
const timestamp = parseInt(parameters.timestamp, 10);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
if (sessionId && timestamp) {
|
|
80
|
+
if (parameters.signature) {
|
|
81
|
+
if (base64UrlVerify.test(parameters.signature)) {
|
|
82
|
+
const signature = fromBase64Url(parameters.signature);
|
|
83
|
+
const { sessionId: _, timestamp: __, signature: ___, ...other } = parameters;
|
|
84
|
+
const otherParameters = Object.entries(other);
|
|
85
|
+
session = await this.validate(sessionId, timestamp, otherParameters, signature);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
throw new Error('Invalid signature format');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
catch (error) {
|
|
84
|
-
|
|
94
|
+
this._onError?.(error);
|
|
85
95
|
}
|
|
86
96
|
if (session) {
|
|
87
97
|
context.set('session', session);
|
package/package.json
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stefanobalocco/honosignedrequests",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "An hono middleware to manage signed requests, including a client implementation.",
|
|
5
|
+
"author": "Stefano Balocco <stefano.balocco@gmail.com>",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/StefanoBalocco/HonoSignedRequests.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/StefanoBalocco/HonoSignedRequests/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/StefanoBalocco/HonoSignedRequests",
|
|
15
|
+
"type": "module",
|
|
5
16
|
"main": "dist/index.js",
|
|
6
17
|
"types": "dist/index.d.ts",
|
|
7
18
|
"exports": {
|
|
@@ -14,16 +25,29 @@
|
|
|
14
25
|
"import": "./client/dist/SignedRequester.min.js"
|
|
15
26
|
}
|
|
16
27
|
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "npm run build:server && npm run build:client && npm run build:tests",
|
|
30
|
+
"build:server": "tsc",
|
|
31
|
+
"build:client": "tsc -p client/tsconfig.json && terser client/dist/SignedRequester.js -o client/dist/SignedRequester.min.js --toplevel -m -c --mangle-props regex=/^_/",
|
|
32
|
+
"build:tests": "tsc -p tests/tsconfig.json",
|
|
33
|
+
"run:tests": "npx ava tests/dist/*.test.js --verbose"
|
|
34
|
+
},
|
|
17
35
|
"devDependencies": {
|
|
36
|
+
"@hono/node-server": "^1.13.7",
|
|
37
|
+
"@types/node": "^22.10.5",
|
|
38
|
+
"ava": "^6.4.1",
|
|
39
|
+
"c8": "^10.1.3",
|
|
18
40
|
"hono": "^4.10.6",
|
|
19
41
|
"terser": "^5.44.1",
|
|
20
|
-
"typescript": "^5.5.3"
|
|
42
|
+
"typescript": "^5.5.3",
|
|
43
|
+
"undici": "^7.4.0"
|
|
21
44
|
},
|
|
22
45
|
"peerDependencies": {
|
|
23
46
|
"hono": "^4.0.0"
|
|
24
47
|
},
|
|
25
48
|
"files": [
|
|
26
|
-
"dist"
|
|
49
|
+
"dist",
|
|
50
|
+
"client/dist"
|
|
27
51
|
],
|
|
28
52
|
"keywords": [
|
|
29
53
|
"hono",
|
|
@@ -32,11 +56,5 @@
|
|
|
32
56
|
"signed-requests",
|
|
33
57
|
"session",
|
|
34
58
|
"authentication"
|
|
35
|
-
]
|
|
36
|
-
|
|
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
|
-
}
|
|
59
|
+
]
|
|
60
|
+
}
|