@lowerdeck/sign 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.
@@ -0,0 +1,13 @@
1
+
2
+ $ microbundle
3
+ No name was provided for external module '@lowerdeck/base62' in output.globals – guessing 'base62'
4
+ No name was provided for external module '@lowerdeck/id' in output.globals – guessing 'id'
5
+ Build "@lowerdeck/sign" to dist:
6
+ 683 B: index.cjs.gz
7
+ 608 B: index.cjs.br
8
+ 581 B: index.modern.js.gz
9
+ 532 B: index.modern.js.br
10
+ 694 B: index.module.js.gz
11
+ 628 B: index.module.js.br
12
+ 776 B: index.umd.js.gz
13
+ 700 B: index.umd.js.br
@@ -0,0 +1,73 @@
1
+
2
+ $ vitest run --passWithNoTests
3
+ [?25l
4
+  RUN  v3.2.4 /Users/tobias/code/metorial/metorial-enterprise/oss/src/packages/backend/sign
5
+
6
+ [?2026h
7
+  ❯ src/sign.test.ts [queued]
8
+
9
+  Test Files 0 passed (1)
10
+  Tests 0 passed (0)
11
+  Start at 10:23:52
12
+  Duration 101ms
13
+ [?2026l[?2026h
14
+  ❯ src/sign.test.ts 1/4
15
+
16
+  Test Files 0 passed (1)
17
+  Tests 1 passed (4)
18
+  Start at 10:23:52
19
+  Duration 306ms
20
+ [?2026l[?2026h
21
+  ❯ src/sign.test.ts 1/4
22
+
23
+  Test Files 0 passed (1)
24
+  Tests 1 passed (4)
25
+  Start at 10:23:52
26
+  Duration 910ms
27
+ [?2026l[?2026h
28
+  ❯ src/sign.test.ts 1/4
29
+
30
+  Test Files 0 passed (1)
31
+  Tests 1 passed (4)
32
+  Start at 10:23:52
33
+  Duration 1.92s
34
+ [?2026l[?2026h
35
+  ❯ src/sign.test.ts 2/4
36
+
37
+  Test Files 0 passed (1)
38
+  Tests 2 passed (4)
39
+  Start at 10:23:52
40
+  Duration 2.22s
41
+ [?2026l[?2026h
42
+  ❯ src/sign.test.ts 2/4
43
+
44
+  Test Files 0 passed (1)
45
+  Tests 2 passed (4)
46
+  Start at 10:23:52
47
+  Duration 2.93s
48
+ [?2026l[?2026h
49
+  ❯ src/sign.test.ts 2/4
50
+
51
+  Test Files 0 passed (1)
52
+  Tests 2 passed (4)
53
+  Start at 10:23:52
54
+  Duration 3.94s
55
+ [?2026l[?2026h
56
+  ❯ src/sign.test.ts 3/4
57
+
58
+  Test Files 0 passed (1)
59
+  Tests 3 passed (4)
60
+  Start at 10:23:52
61
+  Duration 4.24s
62
+ [?2026l ✓ src/sign.test.ts (4 tests) 4008ms
63
+ ✓ sign > sign + verify 3ms
64
+ ✓ sign > expired  2002ms
65
+ ✓ sign > acceptExpired  2003ms
66
+ ✓ sign > start with prefix 0ms
67
+
68
+  Test Files  1 passed (1)
69
+  Tests  4 passed (4)
70
+  Start at  10:23:52
71
+  Duration  4.31s (transform 77ms, setup 0ms, collect 96ms, tests 4.01s, environment 0ms, prepare 53ms)
72
+
73
+ [?25h
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # `@lowerdeck/sign`
2
+
3
+ HMAC signing and verification with expiration support. Uses HMAC-SHA512 and base62 encoding for secure, compact signatures.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @lowerdeck/sign
9
+ yarn add @lowerdeck/sign
10
+ bun add @lowerdeck/sign
11
+ pnpm add @lowerdeck/sign
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### With Expiration
17
+
18
+ ```typescript
19
+ import { signature } from '@lowerdeck/sign';
20
+
21
+ const signer = signature({
22
+ prefix: 'token',
23
+ expirationMs: 3600000, // 1 hour
24
+ key: 'your-secret-key'
25
+ });
26
+
27
+ // Sign data with expiration
28
+ const signed = await signer.sign('user-123');
29
+ console.log(signed); // 'token_xyz123abc...'
30
+
31
+ // Verify signature
32
+ const verified = await signer.verify(signed);
33
+ if (verified) {
34
+ console.log('Valid signature');
35
+ } else {
36
+ console.log('Invalid or expired signature');
37
+ }
38
+ ```
39
+
40
+ ### Without Expiration
41
+
42
+ ```typescript
43
+ import { signatureBasic } from '@lowerdeck/sign';
44
+
45
+ // Simple signing without expiration
46
+ const signed = await signatureBasic.sign('data', 'secret-key');
47
+ console.log(signed); // 'data.xyz123abc...'
48
+
49
+ const isValid = await signatureBasic.verify(signed, 'secret-key');
50
+ console.log(isValid); // true
51
+ ```
52
+
53
+ ## License
54
+
55
+ This project is licensed under the Apache License 2.0.
56
+
57
+ <div align="center">
58
+ <sub>Built with ❤️ by <a href="https://metorial.com">Metorial</a></sub>
59
+ </div>
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ var e=require("@lowerdeck/base62"),r=require("@lowerdeck/id"),t=new Map,n=function(e){try{if(t.has(e))return Promise.resolve(t.get(e));var r=(new TextEncoder).encode(e);return Promise.resolve(crypto.subtle.importKey("raw",r,{name:"HMAC",hash:"SHA-512"},!0,["sign","verify"])).then(function(r){return t.set(e,r),r})}catch(e){return Promise.reject(e)}},i=function(e){return parseInt(e,36)},o=function(e){return(new TextEncoder).encode(e)},s={sign:function(r,t){try{var i=crypto.subtle,s=i.sign;return Promise.resolve(n(t)).then(function(t){return Promise.resolve(s.call(i,"HMAC",t,o(r))).then(function(r){return e.base62.encode(new Uint8Array(r))})})}catch(e){return Promise.reject(e)}},verify:function(r,t,i){try{var s=e.base62.decodeRaw(t),c=crypto.subtle,u=c.verify;return Promise.resolve(n(i)).then(function(e){return u.call(c,"HMAC",e,s,o(r))})}catch(e){return Promise.reject(e)}}};exports.signature=function(t){return{sign:function(i){var s=i.data,c=i.expirationMs,u=void 0===c?t.expirationMs:c;try{return Promise.resolve(n(t.key)).then(function(n){var i=Date.now()+u,c=r.generatePlainId(10),a=JSON.stringify([c,s,i]);return Promise.resolve(crypto.subtle.sign("HMAC",n,o(a))).then(function(r){var n=e.base62.encode(new Uint8Array(r));return""+t.prefix+n+"_"+function(e){return e.toString(36)}(i)+"_"+c})})}catch(e){return Promise.reject(e)}},verify:function(r){var s=r.data,c=r.signature,u=r.acceptExpired,a=void 0!==u&&u;try{return Promise.resolve(n(t.key)).then(function(r){var n=c.slice(t.prefix.length).split("_"),u=n[1],f=n[2],v=e.base62.decodeRaw(n[0]),l=JSON.stringify([f,s,i(u)]);return Promise.resolve(crypto.subtle.verify("HMAC",r,v,o(l))).then(function(e){return!!e&&(!!a||Date.now()<i(u))})})}catch(e){return Promise.reject(e)}}}},exports.signatureBasic=s;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/sign.ts"],"sourcesContent":["import { base62 } from '@lowerdeck/base62';\nimport { generatePlainId } from '@lowerdeck/id';\n\nlet importedKeys = new Map<string, CryptoKey>();\nlet importKey = async (keyStr: string) => {\n if (importedKeys.has(keyStr)) return importedKeys.get(keyStr)!;\n\n let encoder = new TextEncoder();\n let secretKeyData = encoder.encode(keyStr);\n let ck = await crypto.subtle.importKey(\n 'raw',\n secretKeyData,\n { name: 'HMAC', hash: 'SHA-512' },\n true,\n ['sign', 'verify']\n );\n\n importedKeys.set(keyStr, ck);\n\n return ck;\n};\n\nlet exp = {\n stringify: (expiry: number) => expiry.toString(36),\n parse: (expiry: string) => parseInt(expiry, 36)\n};\n\nlet encode = (data: string) => new TextEncoder().encode(data);\n\nexport let signature = (opts: { prefix: string; expirationMs: number; key: string }) => ({\n sign: async ({\n data,\n expirationMs = opts.expirationMs\n }: {\n data: string;\n\n expirationMs?: number;\n }) => {\n let key = await importKey(opts.key);\n let expiry = Date.now() + expirationMs;\n\n let id = generatePlainId(10);\n\n let dataToAuthenticate = JSON.stringify([id, data, expiry]);\n let signature = await crypto.subtle.sign('HMAC', key, encode(dataToAuthenticate));\n\n let hmac = base62.encode(new Uint8Array(signature));\n\n return `${opts.prefix}${hmac}_${exp.stringify(expiry)}_${id}`;\n },\n\n verify: async ({\n data,\n signature,\n acceptExpired = false\n }: {\n data: string;\n signature: string;\n acceptExpired?: boolean;\n }) => {\n let key = await importKey(opts.key);\n\n let sigStr = signature.slice(opts.prefix.length);\n let [encodedHmac, expiry, id] = sigStr.split('_');\n\n let hmac = base62.decodeRaw(encodedHmac);\n\n let dataToAuthenticate = JSON.stringify([id, data, exp.parse(expiry)]);\n\n let isValid = await crypto.subtle.verify('HMAC', key, hmac, encode(dataToAuthenticate));\n\n if (!isValid) return false;\n if (acceptExpired) return true;\n\n return Date.now() < exp.parse(expiry);\n }\n});\n\nexport let signatureBasic = {\n sign: async (data: string, key: string) => {\n let signature = await crypto.subtle.sign('HMAC', await importKey(key), encode(data));\n let hmac = base62.encode(new Uint8Array(signature));\n return hmac;\n },\n\n verify: async (data: string, signature: string, key: string) => {\n let hmac = base62.decodeRaw(signature);\n return crypto.subtle.verify('HMAC', await importKey(key), hmac, encode(data));\n }\n};\n"],"names":["importedKeys","Map","importKey","keyStr","has","Promise","resolve","get","secretKeyData","TextEncoder","encode","crypto","subtle","name","hash","then","ck","set","e","reject","exp","expiry","parseInt","data","signatureBasic","sign","key","_crypto$subtle","_sign","_importKey","call","signature","base62","Uint8Array","verify","hmac","decodeRaw","_crypto$subtle2","_verify","_importKey2","opts","_ref","_ref$expirationMs","expirationMs","Date","now","id","generatePlainId","dataToAuthenticate","JSON","stringify","prefix","toString","_ref2","_ref2$acceptExpired","acceptExpired","_sigStr$split","slice","length","split","isValid"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './sign';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
@@ -0,0 +1,2 @@
1
+ import{base62 as e}from"@lowerdeck/base62";import{generatePlainId as t}from"@lowerdeck/id";let r=new Map,a=async e=>{if(r.has(e))return r.get(e);let t=(new TextEncoder).encode(e),a=await crypto.subtle.importKey("raw",t,{name:"HMAC",hash:"SHA-512"},!0,["sign","verify"]);return r.set(e,a),a},i=e=>parseInt(e,36),n=e=>(new TextEncoder).encode(e),s=r=>({sign:async({data:i,expirationMs:s=r.expirationMs})=>{let o=await a(r.key),c=Date.now()+s,y=t(10),w=JSON.stringify([y,i,c]),p=await crypto.subtle.sign("HMAC",o,n(w)),d=e.encode(new Uint8Array(p));return`${r.prefix}${d}_${(e=>e.toString(36))(c)}_${y}`},verify:async({data:t,signature:s,acceptExpired:o=!1})=>{let c=await a(r.key),y=s.slice(r.prefix.length),[w,p,d]=y.split("_"),l=e.decodeRaw(w),f=JSON.stringify([d,t,i(p)]);return!!await crypto.subtle.verify("HMAC",c,l,n(f))&&(!!o||Date.now()<i(p))}}),o={sign:async(t,r)=>{let i=await crypto.subtle.sign("HMAC",await a(r),n(t));return e.encode(new Uint8Array(i))},verify:async(t,r,i)=>{let s=e.decodeRaw(r);return crypto.subtle.verify("HMAC",await a(i),s,n(t))}};export{s as signature,o as signatureBasic};
2
+ //# sourceMappingURL=index.modern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.modern.js","sources":["../src/sign.ts"],"sourcesContent":["import { base62 } from '@lowerdeck/base62';\nimport { generatePlainId } from '@lowerdeck/id';\n\nlet importedKeys = new Map<string, CryptoKey>();\nlet importKey = async (keyStr: string) => {\n if (importedKeys.has(keyStr)) return importedKeys.get(keyStr)!;\n\n let encoder = new TextEncoder();\n let secretKeyData = encoder.encode(keyStr);\n let ck = await crypto.subtle.importKey(\n 'raw',\n secretKeyData,\n { name: 'HMAC', hash: 'SHA-512' },\n true,\n ['sign', 'verify']\n );\n\n importedKeys.set(keyStr, ck);\n\n return ck;\n};\n\nlet exp = {\n stringify: (expiry: number) => expiry.toString(36),\n parse: (expiry: string) => parseInt(expiry, 36)\n};\n\nlet encode = (data: string) => new TextEncoder().encode(data);\n\nexport let signature = (opts: { prefix: string; expirationMs: number; key: string }) => ({\n sign: async ({\n data,\n expirationMs = opts.expirationMs\n }: {\n data: string;\n\n expirationMs?: number;\n }) => {\n let key = await importKey(opts.key);\n let expiry = Date.now() + expirationMs;\n\n let id = generatePlainId(10);\n\n let dataToAuthenticate = JSON.stringify([id, data, expiry]);\n let signature = await crypto.subtle.sign('HMAC', key, encode(dataToAuthenticate));\n\n let hmac = base62.encode(new Uint8Array(signature));\n\n return `${opts.prefix}${hmac}_${exp.stringify(expiry)}_${id}`;\n },\n\n verify: async ({\n data,\n signature,\n acceptExpired = false\n }: {\n data: string;\n signature: string;\n acceptExpired?: boolean;\n }) => {\n let key = await importKey(opts.key);\n\n let sigStr = signature.slice(opts.prefix.length);\n let [encodedHmac, expiry, id] = sigStr.split('_');\n\n let hmac = base62.decodeRaw(encodedHmac);\n\n let dataToAuthenticate = JSON.stringify([id, data, exp.parse(expiry)]);\n\n let isValid = await crypto.subtle.verify('HMAC', key, hmac, encode(dataToAuthenticate));\n\n if (!isValid) return false;\n if (acceptExpired) return true;\n\n return Date.now() < exp.parse(expiry);\n }\n});\n\nexport let signatureBasic = {\n sign: async (data: string, key: string) => {\n let signature = await crypto.subtle.sign('HMAC', await importKey(key), encode(data));\n let hmac = base62.encode(new Uint8Array(signature));\n return hmac;\n },\n\n verify: async (data: string, signature: string, key: string) => {\n let hmac = base62.decodeRaw(signature);\n return crypto.subtle.verify('HMAC', await importKey(key), hmac, encode(data));\n }\n};\n"],"names":["importedKeys","Map","importKey","async","has","keyStr","get","secretKeyData","TextEncoder","encode","ck","crypto","subtle","name","hash","set","exp","expiry","parseInt","data","signature","opts","sign","expirationMs","key","Date","now","id","generatePlainId","dataToAuthenticate","JSON","stringify","hmac","base62","Uint8Array","prefix","toString","verify","acceptExpired","sigStr","slice","length","encodedHmac","split","decodeRaw","signatureBasic"],"mappings":"2FAGA,IAAIA,EAAe,IAAIC,IACnBC,EAAYC,UACd,GAAIH,EAAaI,IAAIC,GAAS,OAAOL,EAAaM,IAAID,GAEtD,IACIE,GADU,IAAIC,aACUC,OAAOJ,GAC/BK,QAAWC,OAAOC,OAAOV,UAC3B,MACAK,EACA,CAAEM,KAAM,OAAQC,KAAM,YACtB,EACA,CAAC,OAAQ,WAKX,OAFAd,EAAae,IAAIV,EAAQK,GAElBA,GAGLM,EAEMC,GAAmBC,SAASD,EAAQ,IAG1CR,EAAUU,IAAiB,IAAIX,aAAcC,OAAOU,GAE7CC,EAAaC,IAAiE,CACvFC,KAAMnB,OACJgB,OACAI,aAAAA,EAAeF,EAAKE,iBAMpB,IAAIC,QAAYtB,EAAUmB,EAAKG,KAC3BP,EAASQ,KAAKC,MAAQH,EAEtBI,EAAKC,EAAgB,IAErBC,EAAqBC,KAAKC,UAAU,CAACJ,EAAIR,EAAMF,IAC/CG,QAAkBT,OAAOC,OAAOU,KAAK,OAAQE,EAAKf,EAAOoB,IAEzDG,EAAOC,EAAOxB,OAAO,IAAIyB,WAAWd,IAExC,MAAO,GAAGC,EAAKc,SAASH,KAzBdf,IAAmBA,EAAOmB,SAAS,IAyBbpB,CAAcC,MAAWU,KAG3DU,OAAQlC,OACNgB,OACAC,YACAkB,cAAAA,GAAgB,MAMhB,IAAId,QAAYtB,EAAUmB,EAAKG,KAE3Be,EAASnB,EAAUoB,MAAMnB,EAAKc,OAAOM,SACpCC,EAAazB,EAAQU,GAAMY,EAAOI,MAAM,KAEzCX,EAAOC,EAAOW,UAAUF,GAExBb,EAAqBC,KAAKC,UAAU,CAACJ,EAAIR,EAAMH,EAAUC,KAI7D,cAFoBN,OAAOC,OAAOyB,OAAO,OAAQb,EAAKQ,EAAMvB,EAAOoB,QAG/DS,GAEGb,KAAKC,MAAQV,EAAUC,OAIvB4B,EAAiB,CAC1BvB,KAAMnB,MAAOgB,EAAcK,KACzB,IAAIJ,QAAkBT,OAAOC,OAAOU,KAAK,aAAcpB,EAAUsB,GAAMf,EAAOU,IAE9E,OADWc,EAAOxB,OAAO,IAAIyB,WAAWd,KAI1CiB,OAAQlC,MAAOgB,EAAcC,EAAmBI,KAC9C,IAAIQ,EAAOC,EAAOW,UAAUxB,GAC5B,OAAOT,OAAOC,OAAOyB,OAAO,aAAcnC,EAAUsB,GAAMQ,EAAMvB,EAAOU"}
@@ -0,0 +1,2 @@
1
+ import{base62 as e}from"@lowerdeck/base62";import{generatePlainId as r}from"@lowerdeck/id";var t=new Map,n=function(e){try{if(t.has(e))return Promise.resolve(t.get(e));var r=(new TextEncoder).encode(e);return Promise.resolve(crypto.subtle.importKey("raw",r,{name:"HMAC",hash:"SHA-512"},!0,["sign","verify"])).then(function(r){return t.set(e,r),r})}catch(e){return Promise.reject(e)}},o=function(e){return parseInt(e,36)},i=function(e){return(new TextEncoder).encode(e)},c=function(t){return{sign:function(o){var c=o.data,u=o.expirationMs,s=void 0===u?t.expirationMs:u;try{return Promise.resolve(n(t.key)).then(function(n){var o=Date.now()+s,u=r(10),a=JSON.stringify([u,c,o]);return Promise.resolve(crypto.subtle.sign("HMAC",n,i(a))).then(function(r){var n=e.encode(new Uint8Array(r));return""+t.prefix+n+"_"+function(e){return e.toString(36)}(o)+"_"+u})})}catch(e){return Promise.reject(e)}},verify:function(r){var c=r.data,u=r.signature,s=r.acceptExpired,a=void 0!==s&&s;try{return Promise.resolve(n(t.key)).then(function(r){var n=u.slice(t.prefix.length).split("_"),s=n[1],f=n[2],v=e.decodeRaw(n[0]),l=JSON.stringify([f,c,o(s)]);return Promise.resolve(crypto.subtle.verify("HMAC",r,v,i(l))).then(function(e){return!!e&&(!!a||Date.now()<o(s))})})}catch(e){return Promise.reject(e)}}}},u={sign:function(r,t){try{var o=crypto.subtle,c=o.sign;return Promise.resolve(n(t)).then(function(t){return Promise.resolve(c.call(o,"HMAC",t,i(r))).then(function(r){return e.encode(new Uint8Array(r))})})}catch(e){return Promise.reject(e)}},verify:function(r,t,o){try{var c=e.decodeRaw(t),u=crypto.subtle,s=u.verify;return Promise.resolve(n(o)).then(function(e){return s.call(u,"HMAC",e,c,i(r))})}catch(e){return Promise.reject(e)}}};export{c as signature,u as signatureBasic};
2
+ //# sourceMappingURL=index.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.module.js","sources":["../src/sign.ts"],"sourcesContent":["import { base62 } from '@lowerdeck/base62';\nimport { generatePlainId } from '@lowerdeck/id';\n\nlet importedKeys = new Map<string, CryptoKey>();\nlet importKey = async (keyStr: string) => {\n if (importedKeys.has(keyStr)) return importedKeys.get(keyStr)!;\n\n let encoder = new TextEncoder();\n let secretKeyData = encoder.encode(keyStr);\n let ck = await crypto.subtle.importKey(\n 'raw',\n secretKeyData,\n { name: 'HMAC', hash: 'SHA-512' },\n true,\n ['sign', 'verify']\n );\n\n importedKeys.set(keyStr, ck);\n\n return ck;\n};\n\nlet exp = {\n stringify: (expiry: number) => expiry.toString(36),\n parse: (expiry: string) => parseInt(expiry, 36)\n};\n\nlet encode = (data: string) => new TextEncoder().encode(data);\n\nexport let signature = (opts: { prefix: string; expirationMs: number; key: string }) => ({\n sign: async ({\n data,\n expirationMs = opts.expirationMs\n }: {\n data: string;\n\n expirationMs?: number;\n }) => {\n let key = await importKey(opts.key);\n let expiry = Date.now() + expirationMs;\n\n let id = generatePlainId(10);\n\n let dataToAuthenticate = JSON.stringify([id, data, expiry]);\n let signature = await crypto.subtle.sign('HMAC', key, encode(dataToAuthenticate));\n\n let hmac = base62.encode(new Uint8Array(signature));\n\n return `${opts.prefix}${hmac}_${exp.stringify(expiry)}_${id}`;\n },\n\n verify: async ({\n data,\n signature,\n acceptExpired = false\n }: {\n data: string;\n signature: string;\n acceptExpired?: boolean;\n }) => {\n let key = await importKey(opts.key);\n\n let sigStr = signature.slice(opts.prefix.length);\n let [encodedHmac, expiry, id] = sigStr.split('_');\n\n let hmac = base62.decodeRaw(encodedHmac);\n\n let dataToAuthenticate = JSON.stringify([id, data, exp.parse(expiry)]);\n\n let isValid = await crypto.subtle.verify('HMAC', key, hmac, encode(dataToAuthenticate));\n\n if (!isValid) return false;\n if (acceptExpired) return true;\n\n return Date.now() < exp.parse(expiry);\n }\n});\n\nexport let signatureBasic = {\n sign: async (data: string, key: string) => {\n let signature = await crypto.subtle.sign('HMAC', await importKey(key), encode(data));\n let hmac = base62.encode(new Uint8Array(signature));\n return hmac;\n },\n\n verify: async (data: string, signature: string, key: string) => {\n let hmac = base62.decodeRaw(signature);\n return crypto.subtle.verify('HMAC', await importKey(key), hmac, encode(data));\n }\n};\n"],"names":["importedKeys","Map","importKey","keyStr","has","Promise","resolve","get","secretKeyData","TextEncoder","encode","crypto","subtle","name","hash","then","ck","set","e","reject","exp","expiry","parseInt","data","signature","opts","sign","_ref","_ref$expirationMs","expirationMs","key","Date","now","id","generatePlainId","dataToAuthenticate","JSON","stringify","hmac","base62","Uint8Array","prefix","toString","verify","_ref2","_ref2$acceptExpired","acceptExpired","_sigStr$split","slice","length","split","decodeRaw","isValid","signatureBasic","_crypto$subtle","_sign","_importKey","call","_crypto$subtle2","_verify","_importKey2"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@lowerdeck/base62"),require("@lowerdeck/id")):"function"==typeof define&&define.amd?define(["exports","@lowerdeck/base62","@lowerdeck/id"],r):r((e||self).sign={},e.base62,e.id)}(this,function(e,r,n){var t=new Map,i=function(e){try{if(t.has(e))return Promise.resolve(t.get(e));var r=(new TextEncoder).encode(e);return Promise.resolve(crypto.subtle.importKey("raw",r,{name:"HMAC",hash:"SHA-512"},!0,["sign","verify"])).then(function(r){return t.set(e,r),r})}catch(e){return Promise.reject(e)}},o=function(e){return parseInt(e,36)},s=function(e){return(new TextEncoder).encode(e)},c={sign:function(e,n){try{var t=crypto.subtle,o=t.sign;return Promise.resolve(i(n)).then(function(n){return Promise.resolve(o.call(t,"HMAC",n,s(e))).then(function(e){return r.base62.encode(new Uint8Array(e))})})}catch(e){return Promise.reject(e)}},verify:function(e,n,t){try{var o=r.base62.decodeRaw(n),c=crypto.subtle,u=c.verify;return Promise.resolve(i(t)).then(function(r){return u.call(c,"HMAC",r,o,s(e))})}catch(e){return Promise.reject(e)}}};e.signature=function(e){return{sign:function(t){var o=t.data,c=t.expirationMs,u=void 0===c?e.expirationMs:c;try{return Promise.resolve(i(e.key)).then(function(t){var i=Date.now()+u,c=n.generatePlainId(10),a=JSON.stringify([c,o,i]);return Promise.resolve(crypto.subtle.sign("HMAC",t,s(a))).then(function(n){var t=r.base62.encode(new Uint8Array(n));return""+e.prefix+t+"_"+function(e){return e.toString(36)}(i)+"_"+c})})}catch(e){return Promise.reject(e)}},verify:function(n){var t=n.data,c=n.signature,u=n.acceptExpired,a=void 0!==u&&u;try{return Promise.resolve(i(e.key)).then(function(n){var i=c.slice(e.prefix.length).split("_"),u=i[1],f=i[2],d=r.base62.decodeRaw(i[0]),l=JSON.stringify([f,t,o(u)]);return Promise.resolve(crypto.subtle.verify("HMAC",n,d,s(l))).then(function(e){return!!e&&(!!a||Date.now()<o(u))})})}catch(e){return Promise.reject(e)}}}},e.signatureBasic=c});
2
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/sign.ts"],"sourcesContent":["import { base62 } from '@lowerdeck/base62';\nimport { generatePlainId } from '@lowerdeck/id';\n\nlet importedKeys = new Map<string, CryptoKey>();\nlet importKey = async (keyStr: string) => {\n if (importedKeys.has(keyStr)) return importedKeys.get(keyStr)!;\n\n let encoder = new TextEncoder();\n let secretKeyData = encoder.encode(keyStr);\n let ck = await crypto.subtle.importKey(\n 'raw',\n secretKeyData,\n { name: 'HMAC', hash: 'SHA-512' },\n true,\n ['sign', 'verify']\n );\n\n importedKeys.set(keyStr, ck);\n\n return ck;\n};\n\nlet exp = {\n stringify: (expiry: number) => expiry.toString(36),\n parse: (expiry: string) => parseInt(expiry, 36)\n};\n\nlet encode = (data: string) => new TextEncoder().encode(data);\n\nexport let signature = (opts: { prefix: string; expirationMs: number; key: string }) => ({\n sign: async ({\n data,\n expirationMs = opts.expirationMs\n }: {\n data: string;\n\n expirationMs?: number;\n }) => {\n let key = await importKey(opts.key);\n let expiry = Date.now() + expirationMs;\n\n let id = generatePlainId(10);\n\n let dataToAuthenticate = JSON.stringify([id, data, expiry]);\n let signature = await crypto.subtle.sign('HMAC', key, encode(dataToAuthenticate));\n\n let hmac = base62.encode(new Uint8Array(signature));\n\n return `${opts.prefix}${hmac}_${exp.stringify(expiry)}_${id}`;\n },\n\n verify: async ({\n data,\n signature,\n acceptExpired = false\n }: {\n data: string;\n signature: string;\n acceptExpired?: boolean;\n }) => {\n let key = await importKey(opts.key);\n\n let sigStr = signature.slice(opts.prefix.length);\n let [encodedHmac, expiry, id] = sigStr.split('_');\n\n let hmac = base62.decodeRaw(encodedHmac);\n\n let dataToAuthenticate = JSON.stringify([id, data, exp.parse(expiry)]);\n\n let isValid = await crypto.subtle.verify('HMAC', key, hmac, encode(dataToAuthenticate));\n\n if (!isValid) return false;\n if (acceptExpired) return true;\n\n return Date.now() < exp.parse(expiry);\n }\n});\n\nexport let signatureBasic = {\n sign: async (data: string, key: string) => {\n let signature = await crypto.subtle.sign('HMAC', await importKey(key), encode(data));\n let hmac = base62.encode(new Uint8Array(signature));\n return hmac;\n },\n\n verify: async (data: string, signature: string, key: string) => {\n let hmac = base62.decodeRaw(signature);\n return crypto.subtle.verify('HMAC', await importKey(key), hmac, encode(data));\n }\n};\n"],"names":["importedKeys","Map","importKey","keyStr","has","Promise","resolve","get","secretKeyData","TextEncoder","encode","crypto","subtle","name","hash","then","ck","set","e","reject","exp","expiry","parseInt","data","signatureBasic","sign","key","_crypto$subtle","_sign","_importKey","call","signature","base62","Uint8Array","verify","hmac","decodeRaw","_crypto$subtle2","_verify","_importKey2","opts","_ref","_ref$expirationMs","expirationMs","Date","now","id","generatePlainId","dataToAuthenticate","JSON","stringify","prefix","toString","_ref2","_ref2$acceptExpired","acceptExpired","_sigStr$split","slice","length","split","isValid"],"mappings":"0UAGA,IAAIA,EAAe,IAAIC,IACnBC,EAAS,SAAUC,GAAkB,IACvC,GAAIH,EAAaI,IAAID,GAAS,OAAAE,QAAAC,QAAON,EAAaO,IAAIJ,IAEtD,IACIK,GADU,IAAIC,aACUC,OAAOP,GAAQ,OAAAE,QAAAC,QAC5BK,OAAOC,OAAOV,UAC3B,MACAM,EACA,CAAEK,KAAM,OAAQC,KAAM,YACtB,EACA,CAAC,OAAQ,YACVC,cANGC,GAUJ,OAFAhB,EAAaiB,IAAId,EAAQa,GAElBA,CAAG,EACZ,CAAC,MAAAE,UAAAb,QAAAc,OAAAD,KAEGE,EAEK,SAACC,GAAc,OAAKC,SAASD,EAAQ,GAAG,EAG7CX,EAAS,SAACa,UAAqB,IAAAd,aAAcC,OAAOa,EAAK,EAmDlDC,EAAiB,CAC1BC,cAAaF,EAAcG,GAAW,QAAIC,EAClBhB,OAAOC,OAAMgB,EAAbD,EAAcF,KAAIpB,OAAAA,QAAAC,QAAeJ,EAAUwB,IAAIX,KAAAc,SAAAA,GAAAxB,OAAAA,QAAAC,QAAAsB,EAAAE,KAAAH,EAA5B,OAAME,EAAwBnB,EAAOa,KAAKR,KAAA,SAA/EgB,GAEJ,OADWC,SAAOtB,OAAO,IAAIuB,WAAWF,GAC5B,EAAA,EACd,CAAC,MAAAb,GAAAb,OAAAA,QAAAc,OAAAD,EAEDgB,CAAAA,EAAAA,OAAMA,SAASX,EAAcQ,EAAmBL,GAAW,IACzD,IAAIS,EAAOH,SAAOI,UAAUL,GAAWM,EAChC1B,OAAOC,OAAM0B,EAAbD,EAAcH,OAAM,OAAA7B,QAAAC,QAAeJ,EAAUwB,IAAIX,KAAA,SAAAwB,GAAxD,OAAAD,EAAAR,KAAAO,EAA4B,OAAME,EAAwBJ,EAAMzB,EAAOa,GAAO,EAChF,CAAC,MAAAL,UAAAb,QAAAc,OAAAD,kBA3DoB,SAACsB,GAAiE,MAAA,CACvFf,KAAIA,SAAAgB,GACF,IAAAlB,EAAIkB,EAAJlB,KAAImB,EAAAD,EACJE,aAAAA,OAAY,IAAAD,EAAGF,EAAKG,aAAYD,EAAA,IAK7BrC,OAAAA,QAAAC,QACaJ,EAAUsC,EAAKd,MAAIX,cAA/BW,GACJ,IAAIL,EAASuB,KAAKC,MAAQF,EAEtBG,EAAKC,EAAAA,gBAAgB,IAErBC,EAAqBC,KAAKC,UAAU,CAACJ,EAAIvB,EAAMF,IAAS,OAAAhB,QAAAC,QACtCK,OAAOC,OAAOa,KAAK,OAAQC,EAAKhB,EAAOsC,KAAoBjC,KAAA,SAA7EgB,GAEJ,IAAII,EAAOH,EAAMA,OAACtB,OAAO,IAAIuB,WAAWF,IAExC,MAAA,GAAUS,EAAKW,OAAShB,MAzBf,SAACd,GAAc,OAAKA,EAAO+B,SAAS,GAAG,CAyBhBhC,CAAcC,GAAWyB,IAAAA,CAAK,EAChE,EAAA,CAAC,MAAA5B,GAAA,OAAAb,QAAAc,OAAAD,EAAA,CAAA,EAEDgB,OAAM,SAAAmB,GAAA,IACJ9B,EAAI8B,EAAJ9B,KACAQ,EAASsB,EAATtB,UAASuB,EAAAD,EACTE,cAAAA,OAAgB,IAAHD,GAAQA,EAAA,IAKlBjD,OAAAA,QAAAC,QACaJ,EAAUsC,EAAKd,MAAIX,cAA/BW,GAEJ,IACA8B,EADazB,EAAU0B,MAAMjB,EAAKW,OAAOO,QACFC,MAAM,KAA3BtC,EAAMmC,EAAA,GAAEV,EAAEU,KAExBrB,EAAOH,EAAAA,OAAOI,UAFFoB,EAAEnC,IAId2B,EAAqBC,KAAKC,UAAU,CAACJ,EAAIvB,EAAMH,EAAUC,KAAU,OAAAhB,QAAAC,QAEnDK,OAAOC,OAAOsB,OAAO,OAAQR,EAAKS,EAAMzB,EAAOsC,KAAoBjC,KAAnF6C,SAAAA,WAECA,MACDL,GAEGX,KAAKC,MAAQzB,EAAUC,GAHJ,IAI5B,CAAC,MAAAH,GAAAb,OAAAA,QAAAc,OAAAD,EAAA,CAAA,EACF"}
package/dist/sign.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export declare let signature: (opts: {
2
+ prefix: string;
3
+ expirationMs: number;
4
+ key: string;
5
+ }) => {
6
+ sign: ({ data, expirationMs }: {
7
+ data: string;
8
+ expirationMs?: number;
9
+ }) => Promise<string>;
10
+ verify: ({ data, signature, acceptExpired }: {
11
+ data: string;
12
+ signature: string;
13
+ acceptExpired?: boolean;
14
+ }) => Promise<boolean>;
15
+ };
16
+ export declare let signatureBasic: {
17
+ sign: (data: string, key: string) => Promise<string>;
18
+ verify: (data: string, signature: string, key: string) => Promise<boolean>;
19
+ };
20
+ //# sourceMappingURL=sign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AA6BA,eAAO,IAAI,SAAS,GAAI,MAAM;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;mCAI9E;QACD,IAAI,EAAE,MAAM,CAAC;QAEb,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;iDAkBE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB;CAiBD,CAAC;AAEH,eAAO,IAAI,cAAc;iBACJ,MAAM,OAAO,MAAM;mBAMjB,MAAM,aAAa,MAAM,OAAO,MAAM;CAI5D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@lowerdeck/sign",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "author": "Tobias Herber",
8
+ "license": "Apache 2",
9
+ "type": "module",
10
+ "source": "src/index.ts",
11
+ "exports": {
12
+ "require": "./dist/index.cjs",
13
+ "default": "./dist/index.modern.js"
14
+ },
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.module.js",
17
+ "types": "dist/index.d.ts",
18
+ "unpkg": "./dist/index.umd.js",
19
+ "scripts": {
20
+ "test": "vitest run --passWithNoTests",
21
+ "lint": "prettier src/**/*.ts --check",
22
+ "build": "microbundle"
23
+ },
24
+ "dependencies": {
25
+ "@lowerdeck/base62": "^1.0.0",
26
+ "@lowerdeck/id": "^1.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "microbundle": "^0.15.1",
30
+ "@lowerdeck/tsconfig": "^1.0.0",
31
+ "typescript": "5.8.2",
32
+ "vitest": "^3.1.2"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './sign';
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { signature } from './sign';
3
+
4
+ describe('sign', () => {
5
+ test('sign + verify', async () => {
6
+ let key = 'Luw6icRYtGAiWuWp3Qen';
7
+ let data = 'test';
8
+
9
+ let sign = await signature({ prefix: 'abc_', expirationMs: 1000, key }).sign({
10
+ data
11
+ });
12
+
13
+ expect(
14
+ await signature({ prefix: 'abc_', expirationMs: 1000, key }).verify({
15
+ data,
16
+ signature: sign
17
+ })
18
+ ).toBeTruthy();
19
+ });
20
+
21
+ test('expired', async () => {
22
+ let key = 'Luw6icRYtGAiWuWp3Qen';
23
+ let data = 'test';
24
+
25
+ let sign = await signature({ prefix: 'abc_', expirationMs: 1000, key }).sign({
26
+ data
27
+ });
28
+
29
+ expect(
30
+ await signature({ prefix: 'abc_', expirationMs: 1000, key }).verify({
31
+ data,
32
+ signature: sign
33
+ })
34
+ ).toBeTruthy();
35
+
36
+ await new Promise(resolve => setTimeout(resolve, 1000 * 2));
37
+
38
+ expect(
39
+ await signature({ prefix: 'abc_', expirationMs: 1000, key }).verify({
40
+ data,
41
+ signature: sign
42
+ })
43
+ ).toBeFalsy();
44
+ }, 10_000);
45
+
46
+ test('acceptExpired', async () => {
47
+ let key = 'Luw6icRYtGAiWuWp3Qen';
48
+ let data = 'test';
49
+
50
+ let sign = await signature({ prefix: 'abc_', expirationMs: 1000, key }).sign({
51
+ data
52
+ });
53
+
54
+ expect(
55
+ await signature({ prefix: 'abc_', expirationMs: 1000, key }).verify({
56
+ data,
57
+ signature: sign
58
+ })
59
+ ).toBeTruthy();
60
+
61
+ await new Promise(resolve => setTimeout(resolve, 1000 * 2));
62
+
63
+ expect(
64
+ await signature({ prefix: 'abc_', expirationMs: 1000, key }).verify({
65
+ data,
66
+ signature: sign,
67
+ acceptExpired: true
68
+ })
69
+ ).toBeTruthy();
70
+ expect(
71
+ await signature({ prefix: 'abc_', expirationMs: 1000, key }).verify({
72
+ data,
73
+ signature: sign
74
+ })
75
+ ).toBeFalsy();
76
+ }, 10_000);
77
+
78
+ test('start with prefix', async () => {
79
+ let key = 'Luw6icRYtGAiWuWp3Qen';
80
+ let data = 'test';
81
+
82
+ let sign = await signature({ prefix: 'abc_', expirationMs: 1000, key }).sign({
83
+ data
84
+ });
85
+ expect(sign.startsWith('abc_')).toBeTruthy();
86
+ });
87
+ });
package/src/sign.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { base62 } from '@lowerdeck/base62';
2
+ import { generatePlainId } from '@lowerdeck/id';
3
+
4
+ let importedKeys = new Map<string, CryptoKey>();
5
+ let importKey = async (keyStr: string) => {
6
+ if (importedKeys.has(keyStr)) return importedKeys.get(keyStr)!;
7
+
8
+ let encoder = new TextEncoder();
9
+ let secretKeyData = encoder.encode(keyStr);
10
+ let ck = await crypto.subtle.importKey(
11
+ 'raw',
12
+ secretKeyData,
13
+ { name: 'HMAC', hash: 'SHA-512' },
14
+ true,
15
+ ['sign', 'verify']
16
+ );
17
+
18
+ importedKeys.set(keyStr, ck);
19
+
20
+ return ck;
21
+ };
22
+
23
+ let exp = {
24
+ stringify: (expiry: number) => expiry.toString(36),
25
+ parse: (expiry: string) => parseInt(expiry, 36)
26
+ };
27
+
28
+ let encode = (data: string) => new TextEncoder().encode(data);
29
+
30
+ export let signature = (opts: { prefix: string; expirationMs: number; key: string }) => ({
31
+ sign: async ({
32
+ data,
33
+ expirationMs = opts.expirationMs
34
+ }: {
35
+ data: string;
36
+
37
+ expirationMs?: number;
38
+ }) => {
39
+ let key = await importKey(opts.key);
40
+ let expiry = Date.now() + expirationMs;
41
+
42
+ let id = generatePlainId(10);
43
+
44
+ let dataToAuthenticate = JSON.stringify([id, data, expiry]);
45
+ let signature = await crypto.subtle.sign('HMAC', key, encode(dataToAuthenticate));
46
+
47
+ let hmac = base62.encode(new Uint8Array(signature));
48
+
49
+ return `${opts.prefix}${hmac}_${exp.stringify(expiry)}_${id}`;
50
+ },
51
+
52
+ verify: async ({
53
+ data,
54
+ signature,
55
+ acceptExpired = false
56
+ }: {
57
+ data: string;
58
+ signature: string;
59
+ acceptExpired?: boolean;
60
+ }) => {
61
+ let key = await importKey(opts.key);
62
+
63
+ let sigStr = signature.slice(opts.prefix.length);
64
+ let [encodedHmac, expiry, id] = sigStr.split('_');
65
+
66
+ let hmac = base62.decodeRaw(encodedHmac);
67
+
68
+ let dataToAuthenticate = JSON.stringify([id, data, exp.parse(expiry)]);
69
+
70
+ let isValid = await crypto.subtle.verify('HMAC', key, hmac, encode(dataToAuthenticate));
71
+
72
+ if (!isValid) return false;
73
+ if (acceptExpired) return true;
74
+
75
+ return Date.now() < exp.parse(expiry);
76
+ }
77
+ });
78
+
79
+ export let signatureBasic = {
80
+ sign: async (data: string, key: string) => {
81
+ let signature = await crypto.subtle.sign('HMAC', await importKey(key), encode(data));
82
+ let hmac = base62.encode(new Uint8Array(signature));
83
+ return hmac;
84
+ },
85
+
86
+ verify: async (data: string, signature: string, key: string) => {
87
+ let hmac = base62.decodeRaw(signature);
88
+ return crypto.subtle.verify('HMAC', await importKey(key), hmac, encode(data));
89
+ }
90
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@lowerdeck/tsconfig/base.json",
4
+ "exclude": ["dist"],
5
+ "compilerOptions": {
6
+ "outDir": "dist"
7
+ }
8
+ }