@stefanobalocco/honosignedrequests 1.1.1 → 1.2.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/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
 
@@ -144,11 +145,15 @@ These are specific to the in-memory implementation:
144
145
 
145
146
  ```html
146
147
  <script type="module">
147
- import { SignedRequester } from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/HonoSignedRequests/client/dist/SignedRequester.min.js';
148
+ import SignedRequester from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/HonoSignedRequests@1.2.0/client/dist/SignedRequester.min.js';
149
+ // Named import also works:
150
+ // import { SignedRequester } from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/HonoSignedRequests@1.2.0/client/dist/SignedRequester.min.js';
148
151
 
149
152
  const requester = new SignedRequester();
150
- // You can also specify a base URL for request
153
+ // You can also specify a base URL for requests
151
154
  //const requester = new SignedRequester('https://api.example.com');
155
+ // You can also specify an error handler for encoding/decoding errors
156
+ //const requester = new SignedRequester('https://api.example.com', (error) => console.error(error));
152
157
 
153
158
  // Check if we have session data stored
154
159
  let needLogin = true;
@@ -194,6 +199,17 @@ These are specific to the in-memory implementation:
194
199
 
195
200
  ### Client API
196
201
 
202
+ #### Constructor
203
+
204
+ ```javascript
205
+ const requester = new SignedRequester(baseUrl?, onError?);
206
+ ```
207
+
208
+ | Parameter | Type | Description |
209
+ |-----------|------|-------------|
210
+ | `baseUrl` | `string` | Optional base URL for all requests. If not provided, relative paths are used. |
211
+ | `onError` | `(error: unknown) => void` | Optional callback invoked when encoding/decoding errors occur. |
212
+
197
213
  #### `setSession(config)`
198
214
 
199
215
  Initialize session after login.
@@ -36,3 +36,4 @@ declare class SignedRequester {
36
36
  }
37
37
  export type { SessionConfig, SignedRequest, SignedRequestOptions };
38
38
  export { SignedRequester };
39
+ export default SignedRequester;
@@ -16,7 +16,7 @@ function _base64url_encode(value, onError) {
16
16
  }
17
17
  function _base64url_decode(value, onError) {
18
18
  let returnValue;
19
- if (0 < value?.length && /^[A-Za-z0-9_-]*$/.test(value)) {
19
+ if (0 < value?.length && _base64UrlVerify.test(value)) {
20
20
  const padding = value.length % 4;
21
21
  const paddedValue = 0 === padding ? value : value.padEnd(value.length + (4 - padding), '=');
22
22
  const base64 = paddedValue.replace(/-/g, '+').replace(/_/g, '/');
@@ -30,6 +30,7 @@ function _base64url_decode(value, onError) {
30
30
  }
31
31
  return returnValue;
32
32
  }
33
+ const _base64UrlVerify = /^(?=.)(?:[A-Za-z0-9\-_]{4})*(?:[A-Za-z0-9\-_]{2}(?:==)?|[A-Za-z0-9\-_]{3}=?)?$/;
33
34
  class SignedRequester {
34
35
  _semaphoreAcquire(wait = true) {
35
36
  let returnValue = Promise.resolve(false);
@@ -216,3 +217,4 @@ SignedRequester._primitives = new Set([
216
217
  'number'
217
218
  ]);
218
219
  export { SignedRequester };
220
+ export default SignedRequester;
@@ -1 +1 @@
1
- function t(t,e){let s;if(0<t?.length&&/^[A-Za-z0-9_-]*$/.test(t)){const i=t.length%4,o=(0===i?t:t.padEnd(t.length+(4-i),"=")).replace(/-/g,"+").replace(/_/g,"/");try{const t=atob(o);s=Uint8Array.from(t,t=>t.charCodeAt(0))}catch(t){e?.(t)}}return s}class e{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,s,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(s),a=[["sessionId",this.p],["sequenceNumber",this.u],["timestamp",r],...n.sort((t,e)=>t[0].localeCompare(e[0]))].map(([t,s])=>`${t}=${e.q.has(typeof s)||null===s?String(s):JSON.stringify(s)}`).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}}e.q=new Set(["undefined","string","number"]);export{e as SignedRequester};
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};export default s;
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.length % 4)) % 4;
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,4 +1,4 @@
1
- import { constantTimeEqual, fromBase64Url, hmacSha256 } from './Common.js';
1
+ import { base64UrlVerify, constantTimeEqual, fromBase64Url, hmacSha256 } from './Common.js';
2
2
  import { SessionsStorageLocal } from './SessionsStorageLocal.js';
3
3
  export class SignedRequestsManager {
4
4
  static _primitives = new Set(['string', 'number', 'boolean']);
@@ -61,12 +61,13 @@ export class SignedRequestsManager {
61
61
  break;
62
62
  }
63
63
  case 'POST': {
64
- switch (context.req.header('Content-Type')) {
64
+ switch (context.req.header('Content-Type')?.split(';')[0].trim().toLowerCase()) {
65
65
  case 'application/json': {
66
66
  Object.assign(parameters, await context.req.json());
67
67
  break;
68
68
  }
69
- default: {
69
+ case 'multipart/form-data':
70
+ case 'application/x-www-form-urlencoded': {
70
71
  Object.assign(parameters, await context.req.parseBody());
71
72
  break;
72
73
  }
@@ -75,11 +76,18 @@ export class SignedRequestsManager {
75
76
  }
76
77
  const sessionId = parseInt(parameters.sessionId, 10);
77
78
  const timestamp = parseInt(parameters.timestamp, 10);
78
- const signature = fromBase64Url(parameters.signature);
79
- if (sessionId && timestamp && signature) {
80
- const { sessionId: _, timestamp: __, signature: ___, ...other } = parameters;
81
- const otherParameters = Object.entries(other);
82
- session = await this.validate(sessionId, timestamp, otherParameters, signature);
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
+ }
83
91
  }
84
92
  }
85
93
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stefanobalocco/honosignedrequests",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "An hono middleware to manage signed requests, including a client implementation.",
5
5
  "author": "Stefano Balocco <stefano.balocco@gmail.com>",
6
6
  "license": "BSD-3-Clause",