@venn-lang/crypto 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @venn-lang/crypto
2
+
3
+ > The `crypto` namespace: digests, HMACs, base64, PBKDF2 password hashing and JSON Web Tokens.
4
+
5
+ Auth flows need to mint a token, check a signature, or prove that a stored password is not the
6
+ password. This package gives thirteen verbs for that. All of them run over the `CryptoEngine`
7
+ port, whose real implementation is the platform's WebCrypto, so the package stays
8
+ `platform: neutral` and needs no native module.
9
+
10
+ ## Install
11
+
12
+ The package is part of the stdlib the `venn` CLI loads, so nothing to install. Reach it from a
13
+ flow with a `use` line:
14
+
15
+ ```ruby
16
+ use "venn/crypto"
17
+ ```
18
+
19
+ The plugin requires no host capability: cryptography here is computation, not I/O.
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ module demo.session
25
+
26
+ use "venn/assert"
27
+ use "venn/crypto"
28
+
29
+ flow "A session token round-trips" {
30
+ const secret = "s3cret"
31
+
32
+ step "the password is stored hashed, never in the clear" {
33
+ const stored = crypto.password.hash "correct horse" { iterations: 1000 }
34
+ expect stored contains "pbkdf2$sha256$1000$"
35
+
36
+ let ok = crypto.password.verify "correct horse" { hash: stored }
37
+ expect ok == true
38
+
39
+ let wrong = crypto.password.verify "wrong horse" { hash: stored }
40
+ expect wrong == false
41
+ }
42
+
43
+ step "the token verifies, and its claims read back" {
44
+ const token = crypto.jwt.sign { payload: { sub: "alice" }, secret: secret }
45
+
46
+ let ok = crypto.jwt.verify token { secret: secret }
47
+ expect ok == true
48
+
49
+ const decoded = crypto.jwt.decode token
50
+ expect decoded.header.alg == "HS256"
51
+ expect decoded.payload.sub == "alice"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Verbs
57
+
58
+ | Verb | Options | Gives back |
59
+ | --- | --- | --- |
60
+ | `crypto.hash data` | `algorithm` (`sha1`, `sha256`, `sha384`, `sha512`; default `sha256`) | The digest, lowercase hex. |
61
+ | `crypto.hmac data` | `key` (required), `algorithm` | The keyed digest, lowercase hex. |
62
+ | `crypto.randomBytes` | `size` (default 16) | Random bytes, hex-encoded. |
63
+ | `crypto.uuid` | none | A random v4 UUID. |
64
+ | `crypto.base64.encode text` | none | Base64. |
65
+ | `crypto.base64.decode text` | none | The original string. |
66
+ | `crypto.base64url.encode text` | none | Base64url, the flavour JWT uses. |
67
+ | `crypto.base64url.decode text` | none | The original string. |
68
+ | `crypto.jwt.sign` | `payload` (required), `secret` (required), `algorithm` (`HS256`, `HS384`, `HS512`; default `HS256`) | The signed token. |
69
+ | `crypto.jwt.verify token` | `secret` (required) | `true` when the signature matches. |
70
+ | `crypto.jwt.decode token` | none | `crypto.Jwt`: `header`, `payload`, `signature`, `signingInput`. |
71
+ | `crypto.password.hash password` | `iterations` (default 100000), `algorithm` (`sha256` or `sha512`) | The encoded hash. |
72
+ | `crypto.password.verify password` | `hash` (required) | `true` when the password produced that hash. |
73
+
74
+ `crypto.jwt.sign` reads everything from options, so nothing goes in argument position. The rest
75
+ take their subject positionally.
76
+
77
+ ### Passwords
78
+
79
+ `crypto.password.hash` uses PBKDF2, not bcrypt: WebCrypto offers PBKDF2, which keeps this package
80
+ free of a native dependency. The result is self-describing, so verifying needs nothing but the
81
+ string itself:
82
+
83
+ ```
84
+ pbkdf2$sha256$100000$<salt>$<derived>
85
+ ```
86
+
87
+ A fresh 16-byte salt is drawn per call, so hashing the same password twice gives two different
88
+ strings and both verify. Comparison is constant-time.
89
+
90
+ ### Tokens
91
+
92
+ `crypto.jwt.decode` splits and decodes a token **without verifying it**. Reading a token's claims
93
+ and trusting them are two different acts, and `crypto.jwt.verify` is the second one. A token that
94
+ is not base64url-encoded JSON raises `VN7003`. Verification recomputes the HMAC over the token's
95
+ own `signingInput` and compares in constant time, so a payload edited after signing fails.
96
+
97
+ ## The CryptoEngine port
98
+
99
+ `venn.port.crypto-engine`, contract version 1, requires no capability, four methods: `digest`,
100
+ `hmac`, `derive`, `randomBytes`. Everything returns lowercase hex, which is the one shape the
101
+ verbs convert from.
102
+
103
+ | Implementation | Behaviour |
104
+ | --- | --- |
105
+ | `createWebCryptoEngine()` | The real one, backed by `crypto.subtle`. Available in Node 24 and in browsers. |
106
+ | `createFakeCryptoEngine()` | Deterministic FNV-1a stand-in. Same input, same output, never secure; it exists so a flow's assertions replay. |
107
+
108
+ Both run the same conformance suite. The stdlib binds the **real** engine by default, even though
109
+ it binds fakes for everything else: hashing is pure computation, not a side effect, so there is
110
+ nothing to isolate a test from.
111
+
112
+ ## API
113
+
114
+ | Export | What it is |
115
+ | --- | --- |
116
+ | `cryptoPlugin` | The `PluginDefinition` for the `crypto` namespace. |
117
+ | `CryptoEnginePort` | The port descriptor. |
118
+ | `CryptoEngine`, `DeriveArgs`, `HashAlgorithm` | The interface an engine satisfies, its `derive` arguments, and the four digests. |
119
+ | `createWebCryptoEngine`, `createFakeCryptoEngine` | The two implementations. |
120
+ | `decodeJwt(token)`, `DecodedJwt` | The splitter behind `crypto.jwt.decode`, usable directly. |
121
+ | `toBytes`, `fromBytes` | String to `Uint8Array` and back, UTF-8. |
122
+ | `toHex`, `fromHex` | Bytes to lowercase hex and back. |
123
+ | `toBase64`, `fromBase64` | Bytes to base64 and back. |
124
+ | `toBase64Url`, `fromBase64Url` | The same, with `+/` as `-_` and no padding. |
125
+ | `equals(left, right)` | Constant-time string comparison, so a verification cannot be timed. |
126
+
127
+ The plugin publishes one named type, `crypto.Jwt`. Every other verb answers with a string or a
128
+ boolean, which the signature says inline.
129
+
130
+ ## See also
131
+
132
+ - [`@venn-lang/data`](../std-data) for the passwords and identities to hash.
133
+ - [`@venn-lang/auth`](../std-auth) for building the headers a token goes into.
134
+ - [`@venn-lang/contracts`](../contracts) for `Port`, `Host` and capability negotiation.
@@ -0,0 +1,131 @@
1
+ import { Port } from "@venn-lang/contracts";
2
+ import { PluginDefinition } from "@venn-lang/sdk";
3
+ //#region src/bytes/bytes.d.ts
4
+ /** Bytes backed by a plain `ArrayBuffer`, the only shape WebCrypto accepts. */
5
+ type Bytes = Uint8Array<ArrayBuffer>;
6
+ /** UTF-8 encode a string. */
7
+ declare function toBytes(text: string): Bytes;
8
+ /** UTF-8 decode bytes back to a string. Invalid sequences become the replacement character. */
9
+ declare function fromBytes(bytes: Uint8Array): string;
10
+ /** Lowercase hex, two characters per byte. */
11
+ declare function toHex(bytes: Uint8Array): string;
12
+ /** Read a hex string back into bytes. A trailing odd character is dropped. */
13
+ declare function fromHex(hex: string): Bytes;
14
+ /** Standard base64, padded. */
15
+ declare function toBase64(bytes: Uint8Array): string;
16
+ /**
17
+ * Read standard base64 back into bytes.
18
+ *
19
+ * @throws DOMException when the text is not valid base64.
20
+ */
21
+ declare function fromBase64(text: string): Bytes;
22
+ /** Base64url, the flavour JWT uses: `+/` become `-_` and padding is dropped. */
23
+ declare function toBase64Url(bytes: Uint8Array): string;
24
+ /**
25
+ * Read base64url back into bytes, restoring the padding first.
26
+ *
27
+ * @throws DOMException when the text is not valid base64url.
28
+ */
29
+ declare function fromBase64Url(text: string): Bytes;
30
+ /**
31
+ * Compare two strings in time that does not depend on where they differ.
32
+ *
33
+ * Use this for every signature and digest check: `===` returns early on the
34
+ * first mismatched byte, which leaks the correct prefix to a patient attacker.
35
+ */
36
+ declare function equals(left: string, right: string): boolean;
37
+ //#endregion
38
+ //#region src/port/crypto-engine.types.d.ts
39
+ /** The digests every engine supports. */
40
+ type HashAlgorithm = "sha1" | "sha256" | "sha384" | "sha512";
41
+ /** Arguments for {@link CryptoEngine.derive}: a password plus its PBKDF2 cost parameters. */
42
+ interface DeriveArgs {
43
+ password: string;
44
+ salt: string;
45
+ iterations: number;
46
+ algorithm: HashAlgorithm;
47
+ }
48
+ /**
49
+ * The primitives every `crypto` verb is built from.
50
+ *
51
+ * Each method answers in lowercase hex. That single shape is what the actions
52
+ * convert from, so an engine that returned bytes would break every caller.
53
+ */
54
+ interface CryptoEngine {
55
+ digest(args: {
56
+ algorithm: HashAlgorithm;
57
+ data: string;
58
+ }): Promise<string>;
59
+ hmac(args: {
60
+ algorithm: HashAlgorithm;
61
+ key: string;
62
+ data: string;
63
+ }): Promise<string>;
64
+ derive(args: DeriveArgs): Promise<string>;
65
+ randomBytes(size: number): string;
66
+ }
67
+ //#endregion
68
+ //#region src/port/crypto-engine.port.d.ts
69
+ /**
70
+ * The port every `crypto` verb reaches its primitives through.
71
+ *
72
+ * Bound by the host to `createWebCryptoEngine` or `createFakeCryptoEngine`.
73
+ * Requires no capability, because WebCrypto is present on every target.
74
+ */
75
+ declare const CryptoEnginePort: Port<CryptoEngine>;
76
+ //#endregion
77
+ //#region src/engines/fake-engine.d.ts
78
+ /**
79
+ * A deterministic stand-in for tests: same input, same output, no platform crypto.
80
+ *
81
+ * Not secure and not meant to be. It exists so assertions over a digest, an HMAC
82
+ * or a random token stay reproducible from run to run.
83
+ */
84
+ declare function createFakeCryptoEngine(): CryptoEngine;
85
+ //#endregion
86
+ //#region src/engines/web-crypto-engine.d.ts
87
+ /**
88
+ * The real engine, backed by the platform's global WebCrypto.
89
+ *
90
+ * WebCrypto exists in Node 24 and in browsers alike, so nothing here imports
91
+ * `node:crypto` and the package stays platform-neutral.
92
+ */
93
+ declare function createWebCryptoEngine(): CryptoEngine;
94
+ //#endregion
95
+ //#region src/jwt/jwt.types.d.ts
96
+ /** A JWT taken apart, verified or not. */
97
+ interface DecodedJwt {
98
+ header: Record<string, unknown>;
99
+ payload: Record<string, unknown>;
100
+ /** The third section, still base64url. Empty when the token carries none. */
101
+ signature: string;
102
+ /** `header.payload`, exactly the bytes the signature covers. */
103
+ signingInput: string;
104
+ }
105
+ //#endregion
106
+ //#region src/jwt/decode.d.ts
107
+ /**
108
+ * Split a token and decode its header and payload, without verifying anything.
109
+ *
110
+ * Reading a token's claims and trusting them are separate acts. This is the
111
+ * first; `crypto.jwt.verify` is the second.
112
+ *
113
+ * @param token A compact JWS, `header.payload.signature`.
114
+ * @returns The decoded sections plus the `signingInput` a signature covers.
115
+ * @throws VennError `VN7003` when the token has fewer than two sections, or when
116
+ * a section is not base64url-encoded JSON.
117
+ */
118
+ declare function decodeJwt(token: string): DecodedJwt;
119
+ //#endregion
120
+ //#region src/plugin.d.ts
121
+ /**
122
+ * The `crypto` plugin: digests, encodings, password hashing and JSON Web Tokens.
123
+ *
124
+ * Every primitive comes from `CryptoEnginePort`, so a host can swap WebCrypto for
125
+ * the deterministic fake and keep assertions reproducible. No capability is
126
+ * required: WebCrypto is present in Node and in the browser alike.
127
+ */
128
+ declare const cryptoPlugin: PluginDefinition;
129
+ //#endregion
130
+ export { type CryptoEngine, CryptoEnginePort, type DecodedJwt, type DeriveArgs, type HashAlgorithm, createFakeCryptoEngine, createWebCryptoEngine, cryptoPlugin, decodeJwt, equals, fromBase64, fromBase64Url, fromBytes, fromHex, toBase64, toBase64Url, toBytes, toHex };
131
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/bytes/bytes.ts","../src/port/crypto-engine.types.ts","../src/port/crypto-engine.port.ts","../src/engines/fake-engine.ts","../src/engines/web-crypto-engine.ts","../src/jwt/jwt.types.ts","../src/jwt/decode.ts","../src/plugin.ts"],"mappings":";;;;KAIY,QAAQ,WAAW;;iBAGf,QAAQ,eAAe;;iBAKvB,UAAU,OAAO;;iBAKjB,MAAM,OAAO;;iBAKb,QAAQ,cAAc;;iBAMtB,SAAS,OAAO;;;;;;iBAShB,WAAW,eAAe;;iBAK1B,YAAY,OAAO;;;;;;iBASnB,cAAc,eAAe;;;;;;;iBAW7B,OAAO,cAAc;;;;KC7DzB;;UAGK;EACf;EACA;EACA;EACA,WAAW;;;;;;;;UASI;EACf,OAAO;IAAQ,WAAW;IAAe;MAAiB;EAC1D,KAAK;IAAQ,WAAW;IAAe;IAAa;MAAiB;EACrE,OAAO,MAAM,aAAa;EAC1B,YAAY;;;;;;;;;;cCZD,kBAAkB,KAAK;;;;;;;;;iBCDpB,0BAA0B;;;;;;;;;iBCQ1B,yBAAyB;;;;UCfxB;EACf,QAAQ;EACR,SAAS;;EAET;;EAEA;;;;;;;;;;;;;;;iBCQc,UAAU,gBAAgB;;;;;;;;;;cCJ7B,cAAc"}