@kindgi/crypto 0.0.0-bootstrap.0 → 0.1.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 +201 -0
- package/README.md +112 -2
- package/dist/binding.d.ts +60 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +37 -0
- package/dist/binding.js.map +1 -0
- package/dist/ed25519.d.ts +64 -0
- package/dist/ed25519.d.ts.map +1 -0
- package/dist/ed25519.js +169 -0
- package/dist/ed25519.js.map +1 -0
- package/dist/encoding.d.ts +40 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +123 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/hmac.d.ts +16 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/hmac.js +59 -0
- package/dist/hmac.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/webhook.d.ts +74 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +149 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +48 -4
- package/src/binding.ts +103 -0
- package/src/ed25519.ts +228 -0
- package/src/encoding.ts +166 -0
- package/src/errors.ts +84 -0
- package/src/hmac.ts +62 -0
- package/src/index.ts +51 -0
- package/src/webhook.ts +205 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ed25519.js","sourceRoot":"","sources":["../src/ed25519.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC;;;;;;;;;;GAUG;AAEH,OAAO,EAEL,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,IAAI,IAAI,QAAQ,EAChB,MAAM,IAAI,UAAU,GACrB,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACV,OAAO,EACP,QAAQ,EACR,KAAK,EACL,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG/D,uDAAuD;AACvD,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAQ1C;;;;GAIG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,mBAAmB,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE,IAAI,SAAS,CAAC,IAAI,KAAK,KAAK,IAAI,UAAU,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,UAAsB,EACtB,OAAmB;IAEnB,IAAI,CAAC,CAAC,UAAU,YAAY,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,qBAAqB,EAAE,CAAC;QACvF,OAAO,YAAY,CACjB,+BAA+B,qBAAqB,mBAAmB,UAAU,CAAC,MAAM,EAAE,CAC3F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC,EAAE,CAAC;QACrC,OAAO,YAAY,CAAC,sCAAsC,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,SAAoB,CAAC;IACzB,IAAI,CAAC;QACH,SAAS,GAAG,gBAAgB,CAAC;YAC3B,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;YACjD,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,OAAO;SACd,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,YAAY,CAAC,sDAAsD,EAAE,KAAK,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;AAC1D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,SAAqB,EACrB,OAAmB,EACnB,SAAqB;IAErB,IAAI,CAAC,CAAC,SAAS,YAAY,UAAU,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,qBAAqB,EAAE,CAAC;QACrF,OAAO,YAAY,CACjB,8BAA8B,qBAAqB,mBAAmB,SAAS,CAAC,MAAM,EAAE,CACzF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC,EAAE,CAAC;QACrC,OAAO,YAAY,CAAC,sCAAsC,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,CAAC,SAAS,YAAY,UAAU,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,uBAAuB,EAAE,CAAC;QACvF,OAAO,kBAAkB,CACvB,6BAA6B,uBAAuB,eAAe,SAAS,CAAC,MAAM,EAAE,CACtF,CAAC;IACJ,CAAC;IACD,IAAI,SAAoB,CAAC;IACzB,IAAI,CAAC;QACH,SAAS,GAAG,eAAe,CAAC;YAC1B,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAC9C,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,MAAM;SACb,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,YAAY,CAAC,qDAAqD,EAAE,KAAK,CAAC,CAAC;IACpF,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IAC3D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AACnC,CAAC;AAED,kDAAkD;AAElD,MAAM,gBAAgB,GAAG,YAAY,CAAC;AACtC,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAExC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAqB;IACzD,OAAO,KAAK,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC,CAAC;AAC/D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IAC3C,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IACnC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QAC7C,OAAO,YAAY,CACjB,kBAAkB,GAAG,CAAC,KAAK,CAAC,MAAM,2BAA2B,mBAAmB,EAAE,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAsB;IAC3D,OAAO,KAAK,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC;AACnE,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAC5C,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IACnC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,oBAAoB,EAAE,CAAC;QAC9C,OAAO,YAAY,CACjB,kBAAkB,GAAG,CAAC,KAAK,CAAC,MAAM,4BAA4B,oBAAoB,EAAE,CACrF,CAAC;IACJ,CAAC;IACD,OAAO,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,SAAqB;IAC5D,OAAO,QAAQ,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IACnC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QAC7C,OAAO,YAAY,CACjB,qBAAqB,GAAG,CAAC,KAAK,CAAC,MAAM,2BAA2B,mBAAmB,EAAE,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,UAAsB;IAC9D,OAAO,QAAQ,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IACnC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,oBAAoB,EAAE,CAAC;QAC9C,OAAO,YAAY,CACjB,qBAAqB,GAAG,CAAC,KAAK,CAAC,MAAM,4BAA4B,oBAAoB,EAAE,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DER envelope + PEM wrapping helpers for Ed25519 keys.
|
|
3
|
+
*
|
|
4
|
+
* Ed25519 keys have a fixed 32-byte raw form. Interop with Node's
|
|
5
|
+
* `crypto.createPublicKey` / `createPrivateKey` requires wrapping the raw
|
|
6
|
+
* bytes in a standard envelope (SPKI for public, PKCS8 for private) —
|
|
7
|
+
* fixed-prefix DER structures whose only variable part is the 32-byte key
|
|
8
|
+
* material itself. Doing this by hand keeps the package free of ASN.1
|
|
9
|
+
* dependencies and portable across Node / Bun / Deno.
|
|
10
|
+
*
|
|
11
|
+
* The prefixes below encode the AlgorithmIdentifier for Ed25519
|
|
12
|
+
* (OID 1.3.101.112, no parameters) plus the outer SEQUENCE / OCTET STRING
|
|
13
|
+
* wrappers that SPKI (RFC 5280) and PKCS8 (RFC 5958) require. They are
|
|
14
|
+
* constant for Ed25519.
|
|
15
|
+
*/
|
|
16
|
+
import type { Result } from '@kindgi/types';
|
|
17
|
+
import type { MalformedKeyError, UnsupportedFormatError } from './errors.js';
|
|
18
|
+
export declare const ED25519_RAW_KEY_BYTES = 32;
|
|
19
|
+
export declare const ED25519_SPKI_LENGTH: number;
|
|
20
|
+
export declare const ED25519_PKCS8_LENGTH: number;
|
|
21
|
+
/** Wrap a raw 32-byte Ed25519 public key in a DER SPKI envelope. */
|
|
22
|
+
export declare function wrapPublicKeySpki(rawPublicKey: Uint8Array): Uint8Array;
|
|
23
|
+
/** Wrap a raw 32-byte Ed25519 private key seed in a DER PKCS8 envelope. */
|
|
24
|
+
export declare function wrapPrivateKeyPkcs8(rawPrivateKey: Uint8Array): Uint8Array;
|
|
25
|
+
/** Strip the DER SPKI envelope off an Ed25519 public key, returning the 32 raw bytes. */
|
|
26
|
+
export declare function unwrapPublicKeySpki(spki: Uint8Array): Result<Uint8Array, MalformedKeyError>;
|
|
27
|
+
/** Strip the DER PKCS8 envelope off an Ed25519 private key, returning the 32 raw seed bytes. */
|
|
28
|
+
export declare function unwrapPrivateKeyPkcs8(pkcs8: Uint8Array): Result<Uint8Array, MalformedKeyError>;
|
|
29
|
+
/** Encode bytes as standard base64. */
|
|
30
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
31
|
+
/** Decode a base64 string into bytes. Returns a typed error on invalid input. */
|
|
32
|
+
export declare function fromBase64(b64: string): Result<Uint8Array, MalformedKeyError>;
|
|
33
|
+
/** Wrap DER bytes in a PEM envelope with the given label. */
|
|
34
|
+
export declare function toPem(derBytes: Uint8Array, label: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Parse a PEM envelope for the given label. Rejects unexpected labels,
|
|
37
|
+
* missing header/footer, or base64 payload that does not decode cleanly.
|
|
38
|
+
*/
|
|
39
|
+
export declare function fromPem(pem: string, expectedLabel: string): Result<Uint8Array, MalformedKeyError | UnsupportedFormatError>;
|
|
40
|
+
//# sourceMappingURL=encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE7E,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAOxC,eAAO,MAAM,mBAAmB,QAAqD,CAAC;AAOtF,eAAO,MAAM,oBAAoB,QAAsD,CAAC;AAExF,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,UAAU,GAAG,UAAU,CAUtE;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,UAAU,GAAG,UAAU,CAUzE;AAED,yFAAyF;AACzF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAY3F;AAED,gGAAgG;AAChG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAY9F;AAID,uCAAuC;AACvC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAElD;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAkB7E;AAID,6DAA6D;AAC7D,wBAAgB,KAAK,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,UAAU,EAAE,iBAAiB,GAAG,sBAAsB,CAAC,CAwBhE"}
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
import { malformedKey, unsupportedFormat } from './errors.js';
|
|
4
|
+
export const ED25519_RAW_KEY_BYTES = 32;
|
|
5
|
+
// SPKI (SubjectPublicKeyInfo) prefix for Ed25519 (12 bytes). Full envelope
|
|
6
|
+
// is 44 bytes: prefix (12) + raw public key (32).
|
|
7
|
+
const ED25519_SPKI_PREFIX = new Uint8Array([
|
|
8
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
|
|
9
|
+
]);
|
|
10
|
+
export const ED25519_SPKI_LENGTH = ED25519_SPKI_PREFIX.length + ED25519_RAW_KEY_BYTES; // 44
|
|
11
|
+
// PKCS8 (PrivateKeyInfo) prefix for Ed25519 (16 bytes). Full envelope is
|
|
12
|
+
// 48 bytes: prefix (16) + raw private key seed (32).
|
|
13
|
+
const ED25519_PKCS8_PREFIX = new Uint8Array([
|
|
14
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
|
15
|
+
]);
|
|
16
|
+
export const ED25519_PKCS8_LENGTH = ED25519_PKCS8_PREFIX.length + ED25519_RAW_KEY_BYTES; // 48
|
|
17
|
+
/** Wrap a raw 32-byte Ed25519 public key in a DER SPKI envelope. */
|
|
18
|
+
export function wrapPublicKeySpki(rawPublicKey) {
|
|
19
|
+
if (rawPublicKey.length !== ED25519_RAW_KEY_BYTES) {
|
|
20
|
+
throw new Error(`Ed25519 raw public key must be ${ED25519_RAW_KEY_BYTES} bytes, got ${rawPublicKey.length}`);
|
|
21
|
+
}
|
|
22
|
+
const out = new Uint8Array(ED25519_SPKI_LENGTH);
|
|
23
|
+
out.set(ED25519_SPKI_PREFIX, 0);
|
|
24
|
+
out.set(rawPublicKey, ED25519_SPKI_PREFIX.length);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Wrap a raw 32-byte Ed25519 private key seed in a DER PKCS8 envelope. */
|
|
28
|
+
export function wrapPrivateKeyPkcs8(rawPrivateKey) {
|
|
29
|
+
if (rawPrivateKey.length !== ED25519_RAW_KEY_BYTES) {
|
|
30
|
+
throw new Error(`Ed25519 raw private key must be ${ED25519_RAW_KEY_BYTES} bytes, got ${rawPrivateKey.length}`);
|
|
31
|
+
}
|
|
32
|
+
const out = new Uint8Array(ED25519_PKCS8_LENGTH);
|
|
33
|
+
out.set(ED25519_PKCS8_PREFIX, 0);
|
|
34
|
+
out.set(rawPrivateKey, ED25519_PKCS8_PREFIX.length);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** Strip the DER SPKI envelope off an Ed25519 public key, returning the 32 raw bytes. */
|
|
38
|
+
export function unwrapPublicKeySpki(spki) {
|
|
39
|
+
if (spki.length !== ED25519_SPKI_LENGTH) {
|
|
40
|
+
return malformedKey(`Ed25519 SPKI envelope must be ${ED25519_SPKI_LENGTH} bytes, got ${spki.length}`);
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < ED25519_SPKI_PREFIX.length; i++) {
|
|
43
|
+
if (spki[i] !== ED25519_SPKI_PREFIX[i]) {
|
|
44
|
+
return malformedKey('Ed25519 SPKI prefix mismatch — not an Ed25519 public key envelope');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { kind: 'ok', value: spki.slice(ED25519_SPKI_PREFIX.length) };
|
|
48
|
+
}
|
|
49
|
+
/** Strip the DER PKCS8 envelope off an Ed25519 private key, returning the 32 raw seed bytes. */
|
|
50
|
+
export function unwrapPrivateKeyPkcs8(pkcs8) {
|
|
51
|
+
if (pkcs8.length !== ED25519_PKCS8_LENGTH) {
|
|
52
|
+
return malformedKey(`Ed25519 PKCS8 envelope must be ${ED25519_PKCS8_LENGTH} bytes, got ${pkcs8.length}`);
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < ED25519_PKCS8_PREFIX.length; i++) {
|
|
55
|
+
if (pkcs8[i] !== ED25519_PKCS8_PREFIX[i]) {
|
|
56
|
+
return malformedKey('Ed25519 PKCS8 prefix mismatch — not an Ed25519 private key envelope');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { kind: 'ok', value: pkcs8.slice(ED25519_PKCS8_PREFIX.length) };
|
|
60
|
+
}
|
|
61
|
+
// ============ base64 ============
|
|
62
|
+
/** Encode bytes as standard base64. */
|
|
63
|
+
export function toBase64(bytes) {
|
|
64
|
+
return Buffer.from(bytes).toString('base64');
|
|
65
|
+
}
|
|
66
|
+
/** Decode a base64 string into bytes. Returns a typed error on invalid input. */
|
|
67
|
+
export function fromBase64(b64) {
|
|
68
|
+
if (typeof b64 !== 'string' || b64.length === 0) {
|
|
69
|
+
return malformedKey('base64 input must be a non-empty string');
|
|
70
|
+
}
|
|
71
|
+
// Buffer.from is lenient about non-base64 characters; validate strictly.
|
|
72
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) {
|
|
73
|
+
return malformedKey('base64 input contains characters outside the standard alphabet');
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const buf = Buffer.from(b64, 'base64');
|
|
77
|
+
// Round-trip to detect padding / silent truncation.
|
|
78
|
+
if (buf.toString('base64').replace(/=+$/u, '') !== b64.replace(/=+$/u, '')) {
|
|
79
|
+
return malformedKey('base64 input did not round-trip cleanly');
|
|
80
|
+
}
|
|
81
|
+
return { kind: 'ok', value: new Uint8Array(buf) };
|
|
82
|
+
}
|
|
83
|
+
catch (cause) {
|
|
84
|
+
return malformedKey('base64 decode failed', cause);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ============ PEM ============
|
|
88
|
+
/** Wrap DER bytes in a PEM envelope with the given label. */
|
|
89
|
+
export function toPem(derBytes, label) {
|
|
90
|
+
const b64 = toBase64(derBytes);
|
|
91
|
+
const lines = [];
|
|
92
|
+
for (let i = 0; i < b64.length; i += 64)
|
|
93
|
+
lines.push(b64.slice(i, i + 64));
|
|
94
|
+
return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse a PEM envelope for the given label. Rejects unexpected labels,
|
|
98
|
+
* missing header/footer, or base64 payload that does not decode cleanly.
|
|
99
|
+
*/
|
|
100
|
+
export function fromPem(pem, expectedLabel) {
|
|
101
|
+
if (typeof pem !== 'string' || pem.length === 0) {
|
|
102
|
+
return malformedKey('PEM input must be a non-empty string');
|
|
103
|
+
}
|
|
104
|
+
const trimmed = pem.trim();
|
|
105
|
+
const header = `-----BEGIN ${expectedLabel}-----`;
|
|
106
|
+
const footer = `-----END ${expectedLabel}-----`;
|
|
107
|
+
const startIdx = trimmed.indexOf(header);
|
|
108
|
+
const endIdx = trimmed.indexOf(footer);
|
|
109
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
110
|
+
// Detect a *different* PEM label (e.g., someone passed an RSA key here).
|
|
111
|
+
const otherLabelMatch = /-----BEGIN ([^-]+)-----/.exec(trimmed);
|
|
112
|
+
if (otherLabelMatch && otherLabelMatch[1] !== expectedLabel) {
|
|
113
|
+
return unsupportedFormat(`PEM label mismatch — expected "${expectedLabel}", got "${otherLabelMatch[1]}"`);
|
|
114
|
+
}
|
|
115
|
+
return malformedKey(`PEM envelope missing "${expectedLabel}" header/footer`);
|
|
116
|
+
}
|
|
117
|
+
if (endIdx < startIdx) {
|
|
118
|
+
return malformedKey('PEM footer appears before header');
|
|
119
|
+
}
|
|
120
|
+
const body = trimmed.slice(startIdx + header.length, endIdx).replace(/\s+/g, '');
|
|
121
|
+
return fromBase64(body);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.js","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors returned from the crypto primitives. Cryptographic operations
|
|
3
|
+
* fall into two buckets: (a) caller-recoverable errors — malformed key
|
|
4
|
+
* material, wrong-shape input, bad PEM headers — and (b) system-level errors
|
|
5
|
+
* from the underlying primitive that a caller cannot recover from. The
|
|
6
|
+
* former return as `Result<T, CryptoError>`; the latter throw. This file
|
|
7
|
+
* declares (a).
|
|
8
|
+
*/
|
|
9
|
+
export type CryptoError = MalformedKeyError | MalformedSignatureError | UnsupportedFormatError;
|
|
10
|
+
/**
|
|
11
|
+
* Key material could not be parsed. Covers malformed PEM headers, wrong
|
|
12
|
+
* base64, DER envelopes with unexpected OIDs, and byte-length mismatches
|
|
13
|
+
* for the declared algorithm.
|
|
14
|
+
*/
|
|
15
|
+
export interface MalformedKeyError {
|
|
16
|
+
readonly code: 'malformed-key';
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Signature bytes are the wrong length or shape for the declared algorithm.
|
|
22
|
+
* Returned by verify when the caller-supplied signature can be rejected
|
|
23
|
+
* without invoking the underlying primitive.
|
|
24
|
+
*/
|
|
25
|
+
export interface MalformedSignatureError {
|
|
26
|
+
readonly code: 'malformed-signature';
|
|
27
|
+
readonly message: string;
|
|
28
|
+
readonly cause?: unknown;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A serialization format was requested that this package does not support
|
|
32
|
+
* (e.g., a PEM label the parser does not recognize).
|
|
33
|
+
*/
|
|
34
|
+
export interface UnsupportedFormatError {
|
|
35
|
+
readonly code: 'unsupported-format';
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly cause?: unknown;
|
|
38
|
+
}
|
|
39
|
+
export declare function malformedKey(message: string, cause?: unknown): {
|
|
40
|
+
readonly kind: 'err';
|
|
41
|
+
readonly error: MalformedKeyError;
|
|
42
|
+
};
|
|
43
|
+
export declare function malformedSignature(message: string, cause?: unknown): {
|
|
44
|
+
readonly kind: 'err';
|
|
45
|
+
readonly error: MalformedSignatureError;
|
|
46
|
+
};
|
|
47
|
+
export declare function unsupportedFormat(message: string, cause?: unknown): {
|
|
48
|
+
readonly kind: 'err';
|
|
49
|
+
readonly error: UnsupportedFormatError;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,uBAAuB,GAAG,sBAAsB,CAAC;AAE/F;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,CAQ7D;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,uBAAuB,CAAA;CAAE,CAQnE;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAA;CAAE,CAQlE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
export function malformedKey(message, cause) {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'err',
|
|
6
|
+
error: cause === undefined
|
|
7
|
+
? { code: 'malformed-key', message }
|
|
8
|
+
: { code: 'malformed-key', message, cause },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function malformedSignature(message, cause) {
|
|
12
|
+
return {
|
|
13
|
+
kind: 'err',
|
|
14
|
+
error: cause === undefined
|
|
15
|
+
? { code: 'malformed-signature', message }
|
|
16
|
+
: { code: 'malformed-signature', message, cause },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function unsupportedFormat(message, cause) {
|
|
20
|
+
return {
|
|
21
|
+
kind: 'err',
|
|
22
|
+
error: cause === undefined
|
|
23
|
+
? { code: 'unsupported-format', message }
|
|
24
|
+
: { code: 'unsupported-format', message, cause },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AA6CjC,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,KAAe;IAEf,OAAO;QACL,IAAI,EAAE,KAAK;QACX,KAAK,EACH,KAAK,KAAK,SAAS;YACjB,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE;YACpC,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE;KAChD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,OAAe,EACf,KAAe;IAEf,OAAO;QACL,IAAI,EAAE,KAAK;QACX,KAAK,EACH,KAAK,KAAK,SAAS;YACjB,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE;YAC1C,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,KAAK,EAAE;KACtD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,OAAe,EACf,KAAe;IAEf,OAAO;QACL,IAAI,EAAE,KAAK;QACX,KAAK,EACH,KAAK,KAAK,SAAS;YACjB,CAAC,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE;YACzC,CAAC,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,KAAK,EAAE;KACrD,CAAC;AACJ,CAAC"}
|
package/dist/hmac.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the HMAC-SHA256 of `message` under `secret`. Returns the
|
|
3
|
+
* signature hex-encoded (lowercase) — the wire form of the common
|
|
4
|
+
* "hex HMAC over the raw body" webhook convention.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hmacSha256Sign(secret: string | Uint8Array, message: string | Uint8Array): string;
|
|
7
|
+
/**
|
|
8
|
+
* Verify an HMAC-SHA256 signature in constant time. Returns `false` on
|
|
9
|
+
* length mismatch or hex-decode failure (both are shape errors the caller
|
|
10
|
+
* should treat as authentication failure — a well-formed signer would
|
|
11
|
+
* never produce them, so distinguishing them from a genuine mismatch
|
|
12
|
+
* offers no operational value and leaks structural information to an
|
|
13
|
+
* attacker probing the endpoint).
|
|
14
|
+
*/
|
|
15
|
+
export declare function hmacSha256Verify(secret: string | Uint8Array, message: string | Uint8Array, signature: string): boolean;
|
|
16
|
+
//# sourceMappingURL=hmac.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.d.ts","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAeA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAKhG;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAiBT"}
|
package/dist/hmac.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* HMAC-SHA256 message authentication. Symmetric: sender and receiver share
|
|
5
|
+
* a secret. Suited to signing webhook-style payloads — the receiver
|
|
6
|
+
* verifies with the same secret. Follows the common webhook convention
|
|
7
|
+
* of a hex-encoded signature over the raw payload bytes.
|
|
8
|
+
*
|
|
9
|
+
* Verify uses a constant-time comparison to prevent timing side channels
|
|
10
|
+
* from leaking the correct signature byte-by-byte.
|
|
11
|
+
*/
|
|
12
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
13
|
+
/**
|
|
14
|
+
* Compute the HMAC-SHA256 of `message` under `secret`. Returns the
|
|
15
|
+
* signature hex-encoded (lowercase) — the wire form of the common
|
|
16
|
+
* "hex HMAC over the raw body" webhook convention.
|
|
17
|
+
*/
|
|
18
|
+
export function hmacSha256Sign(secret, message) {
|
|
19
|
+
const secretBuf = normalizeSecret(secret);
|
|
20
|
+
const mac = createHmac('sha256', secretBuf);
|
|
21
|
+
mac.update(message);
|
|
22
|
+
return mac.digest('hex');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Verify an HMAC-SHA256 signature in constant time. Returns `false` on
|
|
26
|
+
* length mismatch or hex-decode failure (both are shape errors the caller
|
|
27
|
+
* should treat as authentication failure — a well-formed signer would
|
|
28
|
+
* never produce them, so distinguishing them from a genuine mismatch
|
|
29
|
+
* offers no operational value and leaks structural information to an
|
|
30
|
+
* attacker probing the endpoint).
|
|
31
|
+
*/
|
|
32
|
+
export function hmacSha256Verify(secret, message, signature) {
|
|
33
|
+
if (typeof signature !== 'string')
|
|
34
|
+
return false;
|
|
35
|
+
const expected = hmacSha256Sign(secret, message);
|
|
36
|
+
// Length mismatch is a definitive no; short-circuit before allocating.
|
|
37
|
+
if (expected.length !== signature.length)
|
|
38
|
+
return false;
|
|
39
|
+
// timingSafeEqual requires equal-length buffers. Both are 64-char hex
|
|
40
|
+
// (32-byte SHA-256), pre-verified above.
|
|
41
|
+
let expectedBuf;
|
|
42
|
+
let providedBuf;
|
|
43
|
+
try {
|
|
44
|
+
expectedBuf = Buffer.from(expected, 'hex');
|
|
45
|
+
providedBuf = Buffer.from(signature, 'hex');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (expectedBuf.length !== providedBuf.length)
|
|
51
|
+
return false;
|
|
52
|
+
return timingSafeEqual(expectedBuf, providedBuf);
|
|
53
|
+
}
|
|
54
|
+
function normalizeSecret(secret) {
|
|
55
|
+
if (typeof secret === 'string')
|
|
56
|
+
return Buffer.from(secret, 'utf-8');
|
|
57
|
+
return Buffer.from(secret);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=hmac.js.map
|
package/dist/hmac.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.js","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAA2B,EAAE,OAA4B;IACtF,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC5C,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpB,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAA2B,EAC3B,OAA4B,EAC5B,SAAiB;IAEjB,IAAI,OAAO,SAAS,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,uEAAuE;IACvE,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACvD,sEAAsE;IACtE,yCAAyC;IACzC,IAAI,WAAmB,CAAC;IACxB,IAAI,WAAmB,CAAC;IACxB,IAAI,CAAC;QACH,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC3C,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC5D,OAAO,eAAe,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,eAAe,CAAC,MAA2B;IAClD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpE,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ED25519_SIGNATURE_BYTES, generateEd25519KeyPair, parsePrivateKeyBase64, parsePrivateKeyPem, parsePublicKeyBase64, parsePublicKeyPem, serializePrivateKeyBase64, serializePrivateKeyPem, serializePublicKeyBase64, serializePublicKeyPem, signEd25519, verifyEd25519, } from './ed25519.js';
|
|
2
|
+
export type { Ed25519KeyPair } from './ed25519.js';
|
|
3
|
+
export { hmacSha256Sign, hmacSha256Verify } from './hmac.js';
|
|
4
|
+
export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, generateWebhookSecret, isStrongWebhookSecret, signWebhook, verifyWebhook, WEBHOOK_HEADERS, WEBHOOK_SECRET_MIN_BYTES, WEBHOOK_SECRET_PREFIX, webhookHeaders, } from './webhook.js';
|
|
5
|
+
export type { SignWebhookInput, VerifyWebhookFailure, VerifyWebhookInput, VerifyWebhookResult, WebhookRequestHeaders, } from './webhook.js';
|
|
6
|
+
export { createInMemorySigningKeyBinding } from './binding.js';
|
|
7
|
+
export type { SigningAlgorithm, SigningKeyBinding, SigningKeyDescriptor } from './binding.js';
|
|
8
|
+
export type { CryptoError, MalformedKeyError, MalformedSignatureError, UnsupportedFormatError, } from './errors.js';
|
|
9
|
+
export { ED25519_RAW_KEY_BYTES } from './encoding.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAE9F,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
export { ED25519_SIGNATURE_BYTES, generateEd25519KeyPair, parsePrivateKeyBase64, parsePrivateKeyPem, parsePublicKeyBase64, parsePublicKeyPem, serializePrivateKeyBase64, serializePrivateKeyPem, serializePublicKeyBase64, serializePublicKeyPem, signEd25519, verifyEd25519, } from './ed25519.js';
|
|
4
|
+
export { hmacSha256Sign, hmacSha256Verify } from './hmac.js';
|
|
5
|
+
export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, generateWebhookSecret, isStrongWebhookSecret, signWebhook, verifyWebhook, WEBHOOK_HEADERS, WEBHOOK_SECRET_MIN_BYTES, WEBHOOK_SECRET_PREFIX, webhookHeaders, } from './webhook.js';
|
|
6
|
+
export { createInMemorySigningKeyBinding } from './binding.js';
|
|
7
|
+
export { ED25519_RAW_KEY_BYTES } from './encoding.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,aAAa,GACd,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAC;AAStB,OAAO,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAU/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Result } from '@kindgi/types';
|
|
2
|
+
import type { MalformedKeyError } from './errors.js';
|
|
3
|
+
/** Prefix of a webhook signing secret. */
|
|
4
|
+
export declare const WEBHOOK_SECRET_PREFIX = "whsec_";
|
|
5
|
+
/** The three request headers a signed webhook carries. */
|
|
6
|
+
export declare const WEBHOOK_HEADERS: {
|
|
7
|
+
readonly id: "webhook-id";
|
|
8
|
+
readonly timestamp: "webhook-timestamp";
|
|
9
|
+
readonly signature: "webhook-signature";
|
|
10
|
+
};
|
|
11
|
+
/** How far a message's timestamp may be from the receiver's clock (seconds). */
|
|
12
|
+
export declare const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
13
|
+
/** The fewest random bytes a webhook secret may have (Standard Webhooks: 24–64). */
|
|
14
|
+
export declare const WEBHOOK_SECRET_MIN_BYTES = 24;
|
|
15
|
+
/** A new random signing secret: `whsec_` + base64 of 32 random bytes. */
|
|
16
|
+
export declare function generateWebhookSecret(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a secret is usable for signing: `whsec_` + base64 (or the bare
|
|
19
|
+
* base64) of at least {@link WEBHOOK_SECRET_MIN_BYTES} bytes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isStrongWebhookSecret(secret: string): boolean;
|
|
22
|
+
export interface SignWebhookInput {
|
|
23
|
+
/**
|
|
24
|
+
* The endpoint's secret. During a rotation, pass the new and the
|
|
25
|
+
* previous secret: the header then carries one signature per secret,
|
|
26
|
+
* and a receiver holding either one accepts the message.
|
|
27
|
+
*/
|
|
28
|
+
readonly secret: string | readonly string[];
|
|
29
|
+
/** The message id; stays the same across retries of one message. */
|
|
30
|
+
readonly id: string;
|
|
31
|
+
/** Unix seconds. */
|
|
32
|
+
readonly timestamp: number;
|
|
33
|
+
/** The raw request body, exactly as sent. */
|
|
34
|
+
readonly body: string | Uint8Array;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The `webhook-signature` header value: `v1,<base64>` per secret,
|
|
38
|
+
* space-separated. A malformed secret is a `malformed-key` error. An
|
|
39
|
+
* empty secret list or a timestamp that isn't a non-negative integer is
|
|
40
|
+
* a caller bug and throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function signWebhook(input: SignWebhookInput): Result<string, MalformedKeyError>;
|
|
43
|
+
/** The three signed headers for a message, ready to send. */
|
|
44
|
+
export declare function webhookHeaders(input: SignWebhookInput): Result<Readonly<Record<string, string>>, MalformedKeyError>;
|
|
45
|
+
/** Request headers as a `Headers` object or a plain (Node-style) record. */
|
|
46
|
+
export type WebhookRequestHeaders = {
|
|
47
|
+
get(name: string): string | null;
|
|
48
|
+
} | Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
49
|
+
export interface VerifyWebhookInput {
|
|
50
|
+
readonly secret: string;
|
|
51
|
+
readonly headers: WebhookRequestHeaders;
|
|
52
|
+
/** The raw request body, exactly as received — not a re-serialized JSON value. */
|
|
53
|
+
readonly body: string | Uint8Array;
|
|
54
|
+
/** Default {@link DEFAULT_WEBHOOK_TOLERANCE_SECONDS}. */
|
|
55
|
+
readonly toleranceSeconds?: number;
|
|
56
|
+
/** Clock in milliseconds; default `Date.now`. */
|
|
57
|
+
readonly now?: () => number;
|
|
58
|
+
}
|
|
59
|
+
export type VerifyWebhookFailure = 'missing-headers' | 'invalid-timestamp' | 'timestamp-out-of-tolerance' | 'invalid-secret' | 'no-matching-signature';
|
|
60
|
+
export type VerifyWebhookResult = {
|
|
61
|
+
readonly kind: 'ok';
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly timestamp: number;
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: 'err';
|
|
66
|
+
readonly reason: VerifyWebhookFailure;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Verify a received webhook: the timestamp is within tolerance and at
|
|
70
|
+
* least one `v1` signature in the header matches. Comparison is
|
|
71
|
+
* constant-time. On `ok`, deduplicate on `id` — delivery is at least once.
|
|
72
|
+
*/
|
|
73
|
+
export declare function verifyWebhook(input: VerifyWebhookInput): VerifyWebhookResult;
|
|
74
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAE9C,0DAA0D;AAC1D,eAAO,MAAM,eAAe;;;;CAIlB,CAAC;AAEX,gFAAgF;AAChF,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAKrD,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,yEAAyE;AACzE,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC5C,oEAAoE;IACpE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,oBAAoB;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CActF;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAC5B,KAAK,EAAE,gBAAgB,GACtB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAW7D;AAED,4EAA4E;AAC5E,MAAM,MAAM,qBAAqB,GAC7B;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GACpC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;AAErE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC,yDAAyD;IACzD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,oBAAoB,GAC5B,iBAAiB,GACjB,mBAAmB,GACnB,4BAA4B,GAC5B,gBAAgB,GAChB,uBAAuB,CAAC;AAE5B,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAEpE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,mBAAmB,CA2B5E"}
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Webhook signatures in the Standard Webhooks format
|
|
5
|
+
* (https://www.standardwebhooks.com), symmetric variant `v1`:
|
|
6
|
+
*
|
|
7
|
+
* webhook-id: the message id (the same on every retry)
|
|
8
|
+
* webhook-timestamp: Unix seconds when the message was signed
|
|
9
|
+
* webhook-signature: `v1,<base64 HMAC-SHA256>`, space-separated when
|
|
10
|
+
* several secrets sign (secret rotation)
|
|
11
|
+
*
|
|
12
|
+
* The signed content is `${id}.${timestamp}.${body}`, with `body` the raw
|
|
13
|
+
* request body exactly as sent. Secrets are `whsec_` + base64 of the key
|
|
14
|
+
* bytes. A receiver can verify with this module or with any Standard
|
|
15
|
+
* Webhooks library.
|
|
16
|
+
*/
|
|
17
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
18
|
+
import { malformedKey } from './errors.js';
|
|
19
|
+
/** Prefix of a webhook signing secret. */
|
|
20
|
+
export const WEBHOOK_SECRET_PREFIX = 'whsec_';
|
|
21
|
+
/** The three request headers a signed webhook carries. */
|
|
22
|
+
export const WEBHOOK_HEADERS = {
|
|
23
|
+
id: 'webhook-id',
|
|
24
|
+
timestamp: 'webhook-timestamp',
|
|
25
|
+
signature: 'webhook-signature',
|
|
26
|
+
};
|
|
27
|
+
/** How far a message's timestamp may be from the receiver's clock (seconds). */
|
|
28
|
+
export const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
29
|
+
const SIGNATURE_VERSION = 'v1';
|
|
30
|
+
const SECRET_BYTES = 32;
|
|
31
|
+
/** The fewest random bytes a webhook secret may have (Standard Webhooks: 24–64). */
|
|
32
|
+
export const WEBHOOK_SECRET_MIN_BYTES = 24;
|
|
33
|
+
/** A new random signing secret: `whsec_` + base64 of 32 random bytes. */
|
|
34
|
+
export function generateWebhookSecret() {
|
|
35
|
+
return `${WEBHOOK_SECRET_PREFIX}${randomBytes(SECRET_BYTES).toString('base64')}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a secret is usable for signing: `whsec_` + base64 (or the bare
|
|
39
|
+
* base64) of at least {@link WEBHOOK_SECRET_MIN_BYTES} bytes.
|
|
40
|
+
*/
|
|
41
|
+
export function isStrongWebhookSecret(secret) {
|
|
42
|
+
const key = parseSecret(secret);
|
|
43
|
+
return key !== null && key.length >= WEBHOOK_SECRET_MIN_BYTES;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The `webhook-signature` header value: `v1,<base64>` per secret,
|
|
47
|
+
* space-separated. A malformed secret is a `malformed-key` error. An
|
|
48
|
+
* empty secret list or a timestamp that isn't a non-negative integer is
|
|
49
|
+
* a caller bug and throws.
|
|
50
|
+
*/
|
|
51
|
+
export function signWebhook(input) {
|
|
52
|
+
if (!Number.isSafeInteger(input.timestamp) || input.timestamp < 0) {
|
|
53
|
+
throw new TypeError('signWebhook: timestamp must be a non-negative integer (Unix seconds)');
|
|
54
|
+
}
|
|
55
|
+
const secrets = typeof input.secret === 'string' ? [input.secret] : input.secret;
|
|
56
|
+
if (secrets.length === 0)
|
|
57
|
+
throw new TypeError('signWebhook: at least one secret is required');
|
|
58
|
+
const content = signedContent(input.id, String(input.timestamp), input.body);
|
|
59
|
+
const signatures = [];
|
|
60
|
+
for (const secret of secrets) {
|
|
61
|
+
const key = parseSecret(secret);
|
|
62
|
+
if (key === null)
|
|
63
|
+
return malformedKey('webhook secret must be `whsec_` + base64 key bytes');
|
|
64
|
+
signatures.push(`${SIGNATURE_VERSION},${hmac(key, content).toString('base64')}`);
|
|
65
|
+
}
|
|
66
|
+
return { kind: 'ok', value: signatures.join(' ') };
|
|
67
|
+
}
|
|
68
|
+
/** The three signed headers for a message, ready to send. */
|
|
69
|
+
export function webhookHeaders(input) {
|
|
70
|
+
const signature = signWebhook(input);
|
|
71
|
+
if (signature.kind === 'err')
|
|
72
|
+
return signature;
|
|
73
|
+
return {
|
|
74
|
+
kind: 'ok',
|
|
75
|
+
value: {
|
|
76
|
+
[WEBHOOK_HEADERS.id]: input.id,
|
|
77
|
+
[WEBHOOK_HEADERS.timestamp]: String(input.timestamp),
|
|
78
|
+
[WEBHOOK_HEADERS.signature]: signature.value,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Verify a received webhook: the timestamp is within tolerance and at
|
|
84
|
+
* least one `v1` signature in the header matches. Comparison is
|
|
85
|
+
* constant-time. On `ok`, deduplicate on `id` — delivery is at least once.
|
|
86
|
+
*/
|
|
87
|
+
export function verifyWebhook(input) {
|
|
88
|
+
const id = readHeader(input.headers, WEBHOOK_HEADERS.id);
|
|
89
|
+
const timestampRaw = readHeader(input.headers, WEBHOOK_HEADERS.timestamp);
|
|
90
|
+
const signatureHeader = readHeader(input.headers, WEBHOOK_HEADERS.signature);
|
|
91
|
+
if (id === undefined || timestampRaw === undefined || signatureHeader === undefined) {
|
|
92
|
+
return { kind: 'err', reason: 'missing-headers' };
|
|
93
|
+
}
|
|
94
|
+
if (!/^\d{1,15}$/.test(timestampRaw))
|
|
95
|
+
return { kind: 'err', reason: 'invalid-timestamp' };
|
|
96
|
+
const timestamp = Number(timestampRaw);
|
|
97
|
+
const tolerance = input.toleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
98
|
+
const nowSeconds = Math.floor((input.now ?? Date.now)() / 1000);
|
|
99
|
+
if (Math.abs(nowSeconds - timestamp) > tolerance) {
|
|
100
|
+
return { kind: 'err', reason: 'timestamp-out-of-tolerance' };
|
|
101
|
+
}
|
|
102
|
+
const key = parseSecret(input.secret);
|
|
103
|
+
if (key === null)
|
|
104
|
+
return { kind: 'err', reason: 'invalid-secret' };
|
|
105
|
+
const expected = hmac(key, signedContent(id, timestampRaw, input.body));
|
|
106
|
+
for (const part of signatureHeader.split(' ')) {
|
|
107
|
+
const comma = part.indexOf(',');
|
|
108
|
+
if (comma < 0 || part.slice(0, comma) !== SIGNATURE_VERSION)
|
|
109
|
+
continue;
|
|
110
|
+
const provided = Buffer.from(part.slice(comma + 1), 'base64');
|
|
111
|
+
if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
|
|
112
|
+
return { kind: 'ok', id, timestamp };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { kind: 'err', reason: 'no-matching-signature' };
|
|
116
|
+
}
|
|
117
|
+
function signedContent(id, timestamp, body) {
|
|
118
|
+
const prefix = Buffer.from(`${id}.${timestamp}.`, 'utf-8');
|
|
119
|
+
const bodyBytes = typeof body === 'string' ? Buffer.from(body, 'utf-8') : Buffer.from(body);
|
|
120
|
+
return Buffer.concat([prefix, bodyBytes]);
|
|
121
|
+
}
|
|
122
|
+
function hmac(key, content) {
|
|
123
|
+
return createHmac('sha256', key).update(content).digest();
|
|
124
|
+
}
|
|
125
|
+
/** Key bytes of a `whsec_…` (or bare base64) secret; `null` when malformed. */
|
|
126
|
+
function parseSecret(secret) {
|
|
127
|
+
const encoded = secret.startsWith(WEBHOOK_SECRET_PREFIX)
|
|
128
|
+
? secret.slice(WEBHOOK_SECRET_PREFIX.length)
|
|
129
|
+
: secret;
|
|
130
|
+
if (encoded.length === 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded))
|
|
131
|
+
return null;
|
|
132
|
+
const key = Buffer.from(encoded, 'base64');
|
|
133
|
+
return key.length === 0 ? null : key;
|
|
134
|
+
}
|
|
135
|
+
function readHeader(headers, name) {
|
|
136
|
+
if (typeof headers.get === 'function') {
|
|
137
|
+
const value = headers.get(name);
|
|
138
|
+
return value === null || value.length === 0 ? undefined : value;
|
|
139
|
+
}
|
|
140
|
+
const record = headers;
|
|
141
|
+
for (const [key, value] of Object.entries(record)) {
|
|
142
|
+
if (key.toLowerCase() !== name)
|
|
143
|
+
continue;
|
|
144
|
+
const first = typeof value === 'string' ? value : value?.[0];
|
|
145
|
+
return first === undefined || first.length === 0 ? undefined : first;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=webhook.js.map
|