commits
Sweep through 23 more packages flagged by [monopam lint] for
test-stanza references not declared in opam. Each verified by
[dune build] + [dune runtest] before moving on.
- irmin add (astring :with-test)
- ocaml-atproto-oauth add (eio_main :with-test) (nox-crypto-rng :with-test)
- ocaml-auth add (alcotest :with-test) (eio :with-test) (eio_main :with-test)
- ocaml-cam add (odm :with-test)
- ocaml-cbor add (alcotest :with-test) (ohex :with-test)
- ocaml-cfdp add (nox-csv :with-test)
- ocaml-claude add (vlog :with-test)
- ocaml-collision add (alcotest :with-test) (odm :with-test) (ptime :with-test)
- ocaml-cookie add (re :with-test)
- ocaml-cop1 add (nox-csv :with-test)
- ocaml-crc add (nox-csv :with-test) (nox-memtrace :with-test)
- ocaml-dns-eio add (mdx :with-test); also fix a misplaced
paren in the depends list.
- ocaml-gauth add (nox-crypto-rng :with-test)
- ocaml-http add (alcotest :with-test) (eio_main :with-test) (nox-csv :with-test)
- ocaml-ltp add (nox-csv :with-test)
- ocaml-matter add (ohex :with-test) (ptime :with-test)
- ocaml-oauth add (eio_main :with-test) (nox-crypto-ec :with-test)
- ocaml-ocm add (alcotest :with-test) (nox-csv :with-test)
- ocaml-oem add (alcotest :with-test) (nox-csv :with-test)
- ocaml-opm add (alcotest :with-test) (nox-csv :with-test)
- ocaml-pbkdf2 add (ohex :with-test)
- ocaml-requests add (astring :with-test) (nox-csv :with-test)
- ocaml-retry add (re :with-test)
dune fmt cleanup of an exchange_code example now checked by mdx.
Run mdx on lib/oauth.mli so the two {[ ... ]} odoc blocks now
type-check.
The first block was the begin-redirect / on-callback OAuth flow with
several `(* placeholder *)` comments standing in for branch bodies
and prose comments instead of bindings. Restructured as
`begin_login ()` and `finish_login` functions that take the inputs
they actually need (callback_state, code, etc.).
The second block (Token wrapper) used `Result.get_ok` and trailing
`ignore access`. Replaced with a `acquire_token` helper that takes
real http/clock/credentials, pattern-matches the result via
`Fmt.failwith "%a" Oauth.pp_parse_token_error`, and prints the
access-token length so the value is consumed.
The READMEs all share the standard install/overlay snippet, but the
sh blocks lacked the "<!-- $MDX skip -->" directive. `dune test`
would shell out to `opam install` against the live switch, which
either prompts interactively or fails with a package conflict —
either way diffing as a test failure.
Bulk-add skip directives in front of every install/overlay block.
Also collapse the doubled "non-deterministic + skip" stack on three
READMEs (memtrace, ocaml-dpop, ocaml-pid1, ocaml-yaml, merlint) where
`skip` already implies the runtime is bypassed.
Extends the nox- prefix to the remaining encoding/codec packages —
none clash with opam-repository today, but the rule "blacksun forks
get nox-" applies the same way regardless of conflict status.
Renamed: json, xml, meta, opam, protobuf -> nox-*
Renames 35 packages to make blacksun forks distinguishable from their
opam-repository upstreams. Module names (Git.x, Tls.x, ...) stay bare;
opam package names and dune (public_name) findlib references move to
nox-X. After this commit, zero local package names overlap with
opam-repository.
Renamed:
- nox-git, nox-irmin
- nox-crypto, nox-crypto-pk, nox-crypto-rng, nox-crypto-ec
- nox-tls, nox-tls-eio, nox-tar, nox-tar-eio, nox-tty, nox-tty-eio
- nox-arp, nox-ca-certs, nox-cbor, nox-cookie, nox-crc, nox-csv
- nox-gpt, nox-hkdf, nox-http, nox-jwt, nox-kdf, nox-loc
- nox-memtrace, nox-pds, nox-sexp, nox-slack, nox-toml
- nox-websocket, nox-x509, nox-xdge, nox-yaml
Also drops orphan tar-mirage and tar-unix opam templates that had no
matching package stanza.
Pure formatting changes from `dune fmt`: doc comment placement moves
from above the binding to below it for `type`s, multi-line `match`
expressions collapse onto one line where they fit, and infix operator
applications pick up spaces (`Soup.($?)` -> `Soup.( $? )`). No
semantic changes.
[monopam lint] flagged [loc] as declared-but-not-needed across:
bottler, dupfind, ocaml-agent / auth / freebox / gauth / hap / http /
linkedin / merlin / meross / oauth / osv / paseto / requests / runc /
scaleway / sigstore / spacedata / stix, plus prune and space.
These packages don't import [Loc] directly — they only pull in
[Json.Error] or similar high-level facades, which re-export the
[Loc] surface they need. Drop the stale declaration so the opam
depends stay honest.
- Oauth.Discovery walks the RFC 9728 + RFC 8414 two-step chain:
GET /.well-known/oauth-protected-resource, follow the first
authorization_servers entry, GET /.well-known/oauth-authorization-server.
Handles the RFC 8414 section 3.1 quirk that the well-known suffix is
inserted between origin and path, not appended.
- Oauth.Client models the RFC 7591 Dynamic Client Registration metadata
document plus the RFC 9449 dpop_bound_access_tokens field.
- Oauth.Server.supports / missing let profiles assert policy against the
metadata record (PAR, PKCE method, DPoP alg, grant type, response
type, auth method, scope).
7 new tests. 100 oauth tests pass.
The ATProto OAuth profile's discovery metadata types were spec-level
(RFC 8414 authorization-server metadata with the RFC 9126 PAR and RFC
9449 DPoP extensions, and RFC 9728 protected-resource metadata) with
zero ATProto-specific content. Move them where they belong:
- lib/resource.ml (Oauth.Resource.metadata) is RFC 9728.
- lib/server.ml (Oauth.Server.metadata) is RFC 8414 + PAR + DPoP.
Both carry a Json codec and a pp.
In the same commit: Oauth.Provider.custom is restructured so it
embeds a Server.metadata directly instead of duplicating nine of the
same URLs. Constructor custom_provider keeps its signature (and
builds a minimal Server.metadata under the hood); new helper
provider_of_server builds a custom provider from an already-parsed
discovery document. Access paths that used to be c.token_url now go
through c.server.token_endpoint. This is a breaking API change for
pattern-matching callers, in line with the earlier oauth.ml split.
ocaml-atproto-oauth shrinks to the error type for now; the
ATProto-specific pieces (handle resolution, DID:PLC key discovery,
confidential-client profile policy) will land in follow-ups.
93 oauth tests pass (including 3 resource + 4 server shape tests that
moved over).
Splits oauth.ml into per-concept single-word files (encoding, provider,
redirect, pkce, state, auth, response, exchange, authz, par, flow, token,
userinfo, jws) and makes oauth.ml a thin facade that re-exports them so
the public Oauth.* API is unchanged.
In the same commit: Oauth.Flow ties state + PKCE + optional PAR + optional
DPoP (with RFC 9449 section 8 nonce-challenge retry) into begin_authz /
complete_authz / refresh_bound, and Oauth.Client_auth gets two more RFC
7523 variants:
- Client_auth.secret_jwt signs the client_assertion with HS256 keyed on
the client_secret. The secret never crosses the wire.
- Client_auth.private_key_jwt signs with a Dpop.key (ES256 or EdDSA),
reusing ocaml-dpop's primitives; Dpop exposes a new sign_message for
that purpose.
apply now takes ~aud (the endpoint URL) so the JWT variants can populate
the audience claim. Callers in exchange.ml / par.ml / flow.ml thread
it through.
11 new tests for Client_auth cover the form-field layout, HS256 signature
equality, kid embedding, EdDSA path, and ES256 signature round-trip
verification. 86 tests pass total.
Object combinators: [Object.mem] -> [Object.member], [Object.opt_mem]
-> [Object.opt_member], [Object.case_mem] -> [Object.case_member]. The
sibling submodules [Object.Mem] / [Object.Mems] become
[Object.Member] / [Object.Members]. RFC 8259 §4 calls these
"name/value pairs, referred to as the members", so mirror the spec
name rather than the shortened [mem].
[Object.finish] -> [Object.seal]. "Seal" reads as "close the map, no
more members added", which is what the operation does.
Value constructors/queries: [Value.mem] (function) -> [Value.member];
[Value.mem_find] -> [Value.member_key]; [Value.mem_names] ->
[Value.member_names]; [Value.mem_keys] -> [Value.member_keys].
[type mem = ...] -> [type member = ...]; [type object'] still points
at [member list].
Downstream (~80 files across slack, sbom, stripe, sigstore, requests,
claude, irmin, freebox) updated via perl-pie. dune build clean,
dune test ocaml-json clean.
Replace section marks, en/em dashes, ellipsis, and smart quotes with
ASCII equivalents across oauth.ml and oauth.mli. This aligns the file
with the project's ASCII-only comment rule. No semantic change; the
only non-comment edit is in a couple of error strings (replacing "§"
with "section" inside quoted messages).
The concurrent commit 180d7ab11 landed the new per-module test files
(test_client_auth, test_par, test_provider, test_redirect_uri,
test_authorization_url, test_parse_token_response, test_helpers) but
not the runner or the original kitchen sink. Finish the split:
- Remove test_regressions.ml (tests are now in the per-module files).
- Update test.ml to call the per-module suites.
Breaks the single test.ml runner into one test_<module>.ml per subject:
- test_authorization_url.ml
- test_client_auth.ml
- test_helpers.ml
- test_par.ml
- test_parse_token_response.ml
- test_provider.ml
- test_redirect_uri.ml
Landed here via git-x commit split from an accidentally-bundled commit
in another session's staging; original intent preserved.
Adds an optional par_endpoint to custom providers and an Oauth.Par
module covering the client side of PAR:
- Par.push form-encodes the same authorization parameters
authorization_url would have put in the query string, POSTs them to
par_endpoint under Client_auth, and parses the {request_uri,
expires_in} response. Accepts an optional dpop_proof for servers that
require DPoP on PAR (RFC 9449 §10).
- Par.authorization_url builds the minimal authorization URL carrying
only client_id and request_uri per RFC 9126 §4.
- Par.parse_response decodes the JSON response and distinguishes
missing request_uri, missing expires_in, bad HTTP status, and bad
JSON.
custom_provider now takes an optional ~par_endpoint which is HTTPS-
checked like the other URLs. Built-in providers (GitHub, Google,
GitLab) have no standard PAR endpoint so Par.push returns
No_par_endpoint for them.
8 new tests cover parse success, each parse error, push refusal without
an endpoint, the URL composition carrying only client_id+request_uri,
and HTTPS validation of par_endpoint.
Replaces the scattered ~client_id/~client_secret arguments with a single
Client_auth.t value that captures the authentication method. Supports
none (public client), basic (Authorization header, preferred), and post
(credentials in form body). Basic percent-encodes both halves before
joining with ':' to avoid ambiguity with secrets containing ':' or
non-ASCII bytes.
Callers now pass ~client_auth to exchange_code, refresh_token, Token.make
and Token.of_response. Groundwork for upcoming private_key_jwt and
client_secret_jwt variants (RFC 7523).
42 Json.Codec. prefixes removed. The userinfo record has a `name`
field that clashes with Json.Codec.mem_map.name, so:
- Hoisted accessors `uid`, `login`, `email_verified`, `name`,
`avatar_url` after the userinfo type. `name` carries `(u : userinfo)`
annotation; the others are unique field names so no annotation
needed.
- Each `*_userinfo_jsont` constructor return-types as `: userinfo`
so the inline record literal disambiguates from `mem_map`.
- One stray `decode Value.t` (after the global Json.Codec strip)
restored to `Json.Codec.Value.t` -- it's outside any open scope.
- slack: Json_parse variant stays string; convert Json.Error.t at the
internal api.ml boundary before wrapping.
- meross: Protocol.decode wraps Json.Error.t -> string at boundary.
- stix: bundle_of_string, decode_or_fail, encode_or_fail convert at
boundary.
- oauth: classify_token_error converts incoming Json.Error.t at entry.
- Test files adjusted: Json.Error.to_string for raw Json.of_string calls,
plain strings for package-level APIs that already convert.
Warning 69 (unused-field, mutable-never-assigned). Four independent
record fields were flagged as mutable but the code only mutates their
referents in place, never rebinds the record slot itself:
- ocaml-wal/lib/wal.ml: [t.file] (the Eio file resource; methods call
Eio.File.pwrite_all etc., the slot is set once at open time).
- ocaml-block/lib/block.ml: [Memory.state.data] (the backing bytes,
written via Bytes.blit_string; [Bytes.t] is already mutable).
- ocaml-sse/lib/sse.ml: [Parser.t.data_buf] (a Buffer.t, written via
Buffer.add_*; the slot never changes).
- ocaml-zephyr/lib/zephyr.ml: drop [mode : Read | Write] entirely —
set at open-time, read nowhere. The open_read / open_write
constructors already distinguish the two call shapes, so mode
tracking was redundant.
New S3.Http.put_object_file takes a file path + env, computes the
SigV4 payload hash by streaming the file once through Digestif
(64 KB buffer, no heap allocation beyond the buffer), then hands
Requests.Body.of_file the same path so the HTTP layer streams the
second pass straight to the socket. A 200 MB bottle no longer has
to fit in the OCaml heap.
Sigv4.sign grows an optional ?payload_hash so callers that hash
the body out-of-band (streaming signers, aws-chunked, unsigned
payload) can override the string-based default without rewriting
the signing pipeline.
Bottler.Upload.put_both swaps Bos.OS.File.read + put_object for
put_object_file, removing the last heap-holds-entire-body
behaviour in the upload path.
Commit 5fbed21c switched every cram test fixture from Printf to Fmt
without updating the dune stanzas to depend on fmt, so `dune build`
inside the fixtures fails and the cram expected output stopped
matching reality. Add fmt to each executable/library and refresh the
one stale expected block (cascade_cleanup) still showing Printf.
Generate .opam.template files with x-quality-* fields based on
detected package features:
- x-quality-build: has lib/ with .ml files
- x-quality-test: has test/ with .ml files
- x-quality-fuzz: has fuzz/ with .ml files
- x-quality-interop: has test/interop/ directory
- x-quality-cram: has test/*.t/ directories
These fields are picked up by dune's opam generation and will be
checked by merlint E910 for consistency.
Also: add fmt dep to ocaml-sse/lib/dune (Fmt.pf used without dep).
monopam quality — scans packages for quality features, caches by
git commit hash. 166 packages: build=163, test=162, fuzz=94,
interop=39, doc=42.
Standard vocabulary based on crates.io categories, erratique/opam
conventions, and monorepo domain coverage:
Org: org:blacksun
Domain: aerospace, codec, crypto, network, storage, git, merkle
Purpose: cli, test, bench, format, log, system
Protocol: ccsds, uslp, cop1, sdls, sle, atproto, tls, http, json, binary
Cross-cutting: eio, simulation, math, compression
Tags placed in dune-project (package ...) stanzas via (tags ...).
Propagated to .opam files by dune's opam generation.
- Update .ocamlformat to 0.29.0 across all 591 files
- csvt: reuse single Buffer.t for field reads (no alloc per field)
- sexpt: Obj members decoded from stream into Dict, typed Variant GADT
- Reformat all source files for 0.29.0
- Rename fuzz_github_oauth.ml to fuzz_oauth.ml to match library (E710)
- Convert ocaml-cose fuzz from raw Crowbar to Alcobar with fuzz.ml
runner (E718)
- Fix odoc warnings in sgp4, tomlt, blob_ref
- compute_proof: None branch was dead code producing a predictable
hash. Now fails explicitly.
- verify_receipts: untrusted TSes (ts_keys returns None) are skipped
instead of rejecting the entire bundle. Fails only if no receipt
from a trusted TS verifies. Matches the cross-signing promise in
the .mli documentation.
import used List.find_opt on raw map pairs, so a duplicate
"version" or "entries" key would shadow later occurrences.
Now checks for byte-equal duplicate keys before field extraction,
consistent with the receipt parser's cbor_check_unique_keys.
Custom providers that return the standard OIDC email_verified field
(Section 5.1) now have it respected. Previously hardcoded to false,
email is now only populated when email_verified is true — consistent
with Google and GitLab treatment.
GitLab's /api/v4/user returns confirmed_at as an ISO 8601 timestamp
when the user has verified their email. Parse it and set
email_verified accordingly, instead of hardcoding false.
Email is now only populated when confirmed_at is present, consistent
with the Google and GitHub treatment.
code_verifier is now abstract — can only be created via
generate_code_verifier or code_verifier_of_string (which validates
43-128 chars from [A-Za-z0-9._~-]). Prevents passing low-entropy
strings to code_challenge.
code_verifier_to_string provided for session storage. exchange_code
now takes code_verifier instead of string. 2 new validation tests.
Change status check from >= 400 to outside 200-299 range. A 3xx
redirect from a misconfigured server would either leak client_secret
to the redirect target (if followed) or produce a confusing
Invalid_json error (if not followed). Now any non-2xx status is
rejected cleanly as Http_error.
Test that exchange_code raises Invalid_argument when the Requests.t
handle has verify_tls:false. Also test the Requests.verify_tls getter
returns the correct value for default and explicit false.
Add Requests.verify_tls getter to inspect the TLS setting on a
client handle. Oauth.post_token_endpoint now checks verify_tls and
raises Invalid_argument if certificate verification is disabled,
turning a documentation precondition into a runtime enforcement.
The library validates URL schemes but delegates TLS to the Requests.t
handle. The docs now state the precondition honestly: the handle must
have certificate verification enabled (the Requests.create default).
parse_token_response is a pure JSON parser — it doesn't produce or
transmit secrets. Removing it would destroy 9 valuable edge-case
tests (invalid JSON, missing fields, token_type validation) with no
security benefit. The transport security boundary is at exchange_code
and refresh_token, not at the parser.
These functions exposed client_secret in cleartext and relied on the
caller to POST over TLS. The secure-by-default API is exchange_code
and refresh_token which handle transport internally.
The form encoding functions remain as internal helpers but are no
longer exported. Tests that called them directly have been removed.
The library now depends on requests and performs the token endpoint
POST itself, enforcing TLS transport for client_secret. The old
exchange_form_body and refresh_form_body are kept as low-level
primitives but documented to prefer the new functions.
ocaml-auth's exchange_code now delegates to Oauth.exchange_code.
parse_token_response now parses the token_type field (RFC 6749 §5.1)
and rejects anything other than "bearer" (case-insensitive). Missing
token_type is accepted as Bearer (GitHub omits it). Previously, MAC
or DPoP tokens were silently accepted and would be sent as Bearer,
creating a confused-deputy situation.
New error variant: Unsupported_token_type of string.
4 new tests: rejects mac, accepts Bearer, case-insensitive, missing.
Add email_verified:bool to Oauth.userinfo. Google OIDC sets it from
the email_verified field; GitHub sets it when /user/emails returns a
verified primary; GitLab and custom default to false. The encoder now
faithfully round-trips the verification status.
ocaml-auth now requires email_verified=true to create a user account.
Sign-in is rejected with a clear error if no verified email is
available, instead of fabricating a fake login@provider address.
The encoder wrote email_verified: true whenever email was present,
regardless of the original value. Now omits email_verified on
re-encode since userinfo doesn't track the verification status.
Match both "::1" and "[::1]" for the host to handle Uri library
behavior for IPv6 addresses. Add test for http://[::1]:8080/callback
which was previously claimed but untested.
The previous is_https used a raw prefix check that accepted malformed
URLs like "https://\x00evil.com". Now uses Uri.of_string + Uri.scheme
matching, consistent with redirect_uri validation. Also rejects URLs
with no host component.
Add eqaf to dune-project/opam deps. Make custom_provider record private
so callers must use the smart constructor that enforces HTTPS URLs.
Update tests to use Oauth.custom_provider and cover empty-state rejection.
Replace hand-rolled constant-time string comparison in OAuth
validate_state with Eqaf.equal, and guard against empty state strings.
Fix misleading "most recent" wording in SCITT params doc.
Header.of_cbor now rejects maps with duplicate labels instead of
silently accepting them (first-wins). verify and verify_detached now
check the crit header: if present, all listed labels must be in the
understood set (alg, crit, content_type, kid) or verification fails.
userinfo.email is now string option instead of string. For GitHub,
parse_userinfo intentionally returns None — the /user endpoint only
has the public email which is unverified. Add parse_github_emails to
extract the primary verified email from /user/emails (requires
user:email scope).
ocaml-auth's fetch_userinfo now calls /user/emails for GitHub to
populate the verified email. The type change forces all callers to
handle the missing-email case explicitly.
3 new tests for parse_github_emails. Existing tests updated.
The library generated state but provided no validation function,
leaving callers to roll their own (potentially timing-vulnerable)
comparison or skip validation entirely.
Add validate_state using constant-time byte comparison. Update the
section heading and docs to spell out the caller's obligation: store
state in session before redirect, validate on callback, reject on
mismatch. Update module example to show the full generate/validate
flow.
4 new tests: matching, mismatch, empty, and length-mismatch cases.
custom_provider now checks that path_safe(name) does not equal
"github", "google", or "gitlab". A collision would make callback
routes ambiguous, allowing an attacker-controlled custom provider
to intercept authorization callbacks for a legitimate built-in one.
3 new tests: rejects "GitHub" and "Google" slugs, accepts non-colliding.
Ensure all 67 fuzz/dune files include gen_corpus.exe in the (alias fuzz)
rule deps for AFL corpus generation. Adds both missing runtest and fuzz
rules to ocaml-cose which had neither.
Introduce Oauth.redirect_uri abstract type with a smart constructor
that enforces HTTPS (or http://localhost per RFC 8252 §7.3) and
rejects fragments (RFC 6749 §3.1.2). The type system now prevents
passing unvalidated, user-controlled strings as the redirect target
(RFC 6749 §10.15).
authorization_url and exchange_form_body now take redirect_uri instead
of string. All callers updated. 6 new tests cover the validation rules.
The fuzz test interpolated raw bytes into JSON strings via Fmt.str,
producing malformed JSON for inputs containing quotes or backslashes.
The test then silently accepted parse errors, hiding the breakage.
Replace string interpolation with Jsont encoding, restrict fuzzed
input to printable ASCII for lossless roundtrips, and require parse
success (only empty access_token may fail).
Add custom_provider smart constructor that validates all endpoint URLs
use HTTPS, as required by RFC 6749 for authorization and token
endpoints. Document TLS requirements on exchange_form_body and
refresh_form_body since both transmit client_secret in cleartext.
4 new tests verify rejection of http:// URLs and acceptance of https://.
The dune-project claimed PKCE support but no implementation existed.
Add code_verifier generation, S256/Plain code_challenge computation,
and integrate into authorization_url and exchange_form_body via
optional parameters. Verified against RFC 7636 Appendix B test vector.
New API: generate_code_verifier, code_challenge, challenge_method type.
Updated: authorization_url and exchange_form_body now accept unit arg
with optional PKCE params. All downstream callers in ocaml-auth updated.
10 unit tests + 2 fuzz tests added. Dependencies: digestif, base64.
Resolve E400 (missing documentation), E410 (bad doc style), E615
(missing test suite), and E616 (use failf) across the monorepo.
Also fix test_timing to reference Requests.Timing instead of
non-existent Http.Timing.
Renames: find_vds_info → vds_info, make_unique_indexes → unique_indexes,
create_new → new_db. Extract err_userinfo_http, err_userinfo_parse,
err_userinfo_empty_uid helpers for consistent error formatting (E340).
Irmin:
- MST codec keyed by Atp.Cid.t (removed Hash↔CID conversion layer)
- Backend.{Memory,Disk}.create_cid — CID-native backends
- Proof.encode_cbor / decode_cbor — CBOR serialization for COSE receipts
- pds_interop: trivial passthrough (no conversion needed)
- 72 tests pass
SCITT:
- Receipt vds (395) in protected header per COSE Receipts spec
- Receipt vdp (396) in unprotected header for proof data
- RFC 9162 VDS: O(1) amortized append, RFC-compliant verify_inclusion
with test vectors from the spec
- MST VDS: Irmin.Proof.Mst.produce at registration, encode_cbor into
receipt, decode + Irmin.Proof.Mst.verify for fully offline verification
- Leaf hash authentication binds proof to specific statement
- 34 main + 17 ATP = 51 tests, all pass
Sigstore:
- Certificate chain validation against Fulcio root CA
- Rekor entry binding (body/log_index/integrated_time comparison)
- Hash algorithm from bundle (not hardcoded SHA256)
- 58 tests pass
Auth:
- Per-provider callback URLs (/auth/<slug>/callback)
- provider_name (raw, for DB) vs provider_slug (URL-safe, for routes)
- Token exchange includes grant_type=authorization_code
- No credential leakage in logs or error responses
Respond:
- HEAD responses suppress body per RFC 9110 §9.3.2
OAuth:
- Provider variant type (Github | Google | Gitlab | Custom)
- Per-provider userinfo JSON schemas (no field guessing)
Sweep through 23 more packages flagged by [monopam lint] for
test-stanza references not declared in opam. Each verified by
[dune build] + [dune runtest] before moving on.
- irmin add (astring :with-test)
- ocaml-atproto-oauth add (eio_main :with-test) (nox-crypto-rng :with-test)
- ocaml-auth add (alcotest :with-test) (eio :with-test) (eio_main :with-test)
- ocaml-cam add (odm :with-test)
- ocaml-cbor add (alcotest :with-test) (ohex :with-test)
- ocaml-cfdp add (nox-csv :with-test)
- ocaml-claude add (vlog :with-test)
- ocaml-collision add (alcotest :with-test) (odm :with-test) (ptime :with-test)
- ocaml-cookie add (re :with-test)
- ocaml-cop1 add (nox-csv :with-test)
- ocaml-crc add (nox-csv :with-test) (nox-memtrace :with-test)
- ocaml-dns-eio add (mdx :with-test); also fix a misplaced
paren in the depends list.
- ocaml-gauth add (nox-crypto-rng :with-test)
- ocaml-http add (alcotest :with-test) (eio_main :with-test) (nox-csv :with-test)
- ocaml-ltp add (nox-csv :with-test)
- ocaml-matter add (ohex :with-test) (ptime :with-test)
- ocaml-oauth add (eio_main :with-test) (nox-crypto-ec :with-test)
- ocaml-ocm add (alcotest :with-test) (nox-csv :with-test)
- ocaml-oem add (alcotest :with-test) (nox-csv :with-test)
- ocaml-opm add (alcotest :with-test) (nox-csv :with-test)
- ocaml-pbkdf2 add (ohex :with-test)
- ocaml-requests add (astring :with-test) (nox-csv :with-test)
- ocaml-retry add (re :with-test)
Run mdx on lib/oauth.mli so the two {[ ... ]} odoc blocks now
type-check.
The first block was the begin-redirect / on-callback OAuth flow with
several `(* placeholder *)` comments standing in for branch bodies
and prose comments instead of bindings. Restructured as
`begin_login ()` and `finish_login` functions that take the inputs
they actually need (callback_state, code, etc.).
The second block (Token wrapper) used `Result.get_ok` and trailing
`ignore access`. Replaced with a `acquire_token` helper that takes
real http/clock/credentials, pattern-matches the result via
`Fmt.failwith "%a" Oauth.pp_parse_token_error`, and prints the
access-token length so the value is consumed.
The READMEs all share the standard install/overlay snippet, but the
sh blocks lacked the "<!-- $MDX skip -->" directive. `dune test`
would shell out to `opam install` against the live switch, which
either prompts interactively or fails with a package conflict —
either way diffing as a test failure.
Bulk-add skip directives in front of every install/overlay block.
Also collapse the doubled "non-deterministic + skip" stack on three
READMEs (memtrace, ocaml-dpop, ocaml-pid1, ocaml-yaml, merlint) where
`skip` already implies the runtime is bypassed.
Renames 35 packages to make blacksun forks distinguishable from their
opam-repository upstreams. Module names (Git.x, Tls.x, ...) stay bare;
opam package names and dune (public_name) findlib references move to
nox-X. After this commit, zero local package names overlap with
opam-repository.
Renamed:
- nox-git, nox-irmin
- nox-crypto, nox-crypto-pk, nox-crypto-rng, nox-crypto-ec
- nox-tls, nox-tls-eio, nox-tar, nox-tar-eio, nox-tty, nox-tty-eio
- nox-arp, nox-ca-certs, nox-cbor, nox-cookie, nox-crc, nox-csv
- nox-gpt, nox-hkdf, nox-http, nox-jwt, nox-kdf, nox-loc
- nox-memtrace, nox-pds, nox-sexp, nox-slack, nox-toml
- nox-websocket, nox-x509, nox-xdge, nox-yaml
Also drops orphan tar-mirage and tar-unix opam templates that had no
matching package stanza.
[monopam lint] flagged [loc] as declared-but-not-needed across:
bottler, dupfind, ocaml-agent / auth / freebox / gauth / hap / http /
linkedin / merlin / meross / oauth / osv / paseto / requests / runc /
scaleway / sigstore / spacedata / stix, plus prune and space.
These packages don't import [Loc] directly — they only pull in
[Json.Error] or similar high-level facades, which re-export the
[Loc] surface they need. Drop the stale declaration so the opam
depends stay honest.
- Oauth.Discovery walks the RFC 9728 + RFC 8414 two-step chain:
GET /.well-known/oauth-protected-resource, follow the first
authorization_servers entry, GET /.well-known/oauth-authorization-server.
Handles the RFC 8414 section 3.1 quirk that the well-known suffix is
inserted between origin and path, not appended.
- Oauth.Client models the RFC 7591 Dynamic Client Registration metadata
document plus the RFC 9449 dpop_bound_access_tokens field.
- Oauth.Server.supports / missing let profiles assert policy against the
metadata record (PAR, PKCE method, DPoP alg, grant type, response
type, auth method, scope).
7 new tests. 100 oauth tests pass.
The ATProto OAuth profile's discovery metadata types were spec-level
(RFC 8414 authorization-server metadata with the RFC 9126 PAR and RFC
9449 DPoP extensions, and RFC 9728 protected-resource metadata) with
zero ATProto-specific content. Move them where they belong:
- lib/resource.ml (Oauth.Resource.metadata) is RFC 9728.
- lib/server.ml (Oauth.Server.metadata) is RFC 8414 + PAR + DPoP.
Both carry a Json codec and a pp.
In the same commit: Oauth.Provider.custom is restructured so it
embeds a Server.metadata directly instead of duplicating nine of the
same URLs. Constructor custom_provider keeps its signature (and
builds a minimal Server.metadata under the hood); new helper
provider_of_server builds a custom provider from an already-parsed
discovery document. Access paths that used to be c.token_url now go
through c.server.token_endpoint. This is a breaking API change for
pattern-matching callers, in line with the earlier oauth.ml split.
ocaml-atproto-oauth shrinks to the error type for now; the
ATProto-specific pieces (handle resolution, DID:PLC key discovery,
confidential-client profile policy) will land in follow-ups.
93 oauth tests pass (including 3 resource + 4 server shape tests that
moved over).
Splits oauth.ml into per-concept single-word files (encoding, provider,
redirect, pkce, state, auth, response, exchange, authz, par, flow, token,
userinfo, jws) and makes oauth.ml a thin facade that re-exports them so
the public Oauth.* API is unchanged.
In the same commit: Oauth.Flow ties state + PKCE + optional PAR + optional
DPoP (with RFC 9449 section 8 nonce-challenge retry) into begin_authz /
complete_authz / refresh_bound, and Oauth.Client_auth gets two more RFC
7523 variants:
- Client_auth.secret_jwt signs the client_assertion with HS256 keyed on
the client_secret. The secret never crosses the wire.
- Client_auth.private_key_jwt signs with a Dpop.key (ES256 or EdDSA),
reusing ocaml-dpop's primitives; Dpop exposes a new sign_message for
that purpose.
apply now takes ~aud (the endpoint URL) so the JWT variants can populate
the audience claim. Callers in exchange.ml / par.ml / flow.ml thread
it through.
11 new tests for Client_auth cover the form-field layout, HS256 signature
equality, kid embedding, EdDSA path, and ES256 signature round-trip
verification. 86 tests pass total.
Object combinators: [Object.mem] -> [Object.member], [Object.opt_mem]
-> [Object.opt_member], [Object.case_mem] -> [Object.case_member]. The
sibling submodules [Object.Mem] / [Object.Mems] become
[Object.Member] / [Object.Members]. RFC 8259 §4 calls these
"name/value pairs, referred to as the members", so mirror the spec
name rather than the shortened [mem].
[Object.finish] -> [Object.seal]. "Seal" reads as "close the map, no
more members added", which is what the operation does.
Value constructors/queries: [Value.mem] (function) -> [Value.member];
[Value.mem_find] -> [Value.member_key]; [Value.mem_names] ->
[Value.member_names]; [Value.mem_keys] -> [Value.member_keys].
[type mem = ...] -> [type member = ...]; [type object'] still points
at [member list].
Downstream (~80 files across slack, sbom, stripe, sigstore, requests,
claude, irmin, freebox) updated via perl-pie. dune build clean,
dune test ocaml-json clean.
Replace section marks, en/em dashes, ellipsis, and smart quotes with
ASCII equivalents across oauth.ml and oauth.mli. This aligns the file
with the project's ASCII-only comment rule. No semantic change; the
only non-comment edit is in a couple of error strings (replacing "§"
with "section" inside quoted messages).
The concurrent commit 180d7ab11 landed the new per-module test files
(test_client_auth, test_par, test_provider, test_redirect_uri,
test_authorization_url, test_parse_token_response, test_helpers) but
not the runner or the original kitchen sink. Finish the split:
- Remove test_regressions.ml (tests are now in the per-module files).
- Update test.ml to call the per-module suites.
Breaks the single test.ml runner into one test_<module>.ml per subject:
- test_authorization_url.ml
- test_client_auth.ml
- test_helpers.ml
- test_par.ml
- test_parse_token_response.ml
- test_provider.ml
- test_redirect_uri.ml
Landed here via git-x commit split from an accidentally-bundled commit
in another session's staging; original intent preserved.
Adds an optional par_endpoint to custom providers and an Oauth.Par
module covering the client side of PAR:
- Par.push form-encodes the same authorization parameters
authorization_url would have put in the query string, POSTs them to
par_endpoint under Client_auth, and parses the {request_uri,
expires_in} response. Accepts an optional dpop_proof for servers that
require DPoP on PAR (RFC 9449 §10).
- Par.authorization_url builds the minimal authorization URL carrying
only client_id and request_uri per RFC 9126 §4.
- Par.parse_response decodes the JSON response and distinguishes
missing request_uri, missing expires_in, bad HTTP status, and bad
JSON.
custom_provider now takes an optional ~par_endpoint which is HTTPS-
checked like the other URLs. Built-in providers (GitHub, Google,
GitLab) have no standard PAR endpoint so Par.push returns
No_par_endpoint for them.
8 new tests cover parse success, each parse error, push refusal without
an endpoint, the URL composition carrying only client_id+request_uri,
and HTTPS validation of par_endpoint.
Replaces the scattered ~client_id/~client_secret arguments with a single
Client_auth.t value that captures the authentication method. Supports
none (public client), basic (Authorization header, preferred), and post
(credentials in form body). Basic percent-encodes both halves before
joining with ':' to avoid ambiguity with secrets containing ':' or
non-ASCII bytes.
Callers now pass ~client_auth to exchange_code, refresh_token, Token.make
and Token.of_response. Groundwork for upcoming private_key_jwt and
client_secret_jwt variants (RFC 7523).
42 Json.Codec. prefixes removed. The userinfo record has a `name`
field that clashes with Json.Codec.mem_map.name, so:
- Hoisted accessors `uid`, `login`, `email_verified`, `name`,
`avatar_url` after the userinfo type. `name` carries `(u : userinfo)`
annotation; the others are unique field names so no annotation
needed.
- Each `*_userinfo_jsont` constructor return-types as `: userinfo`
so the inline record literal disambiguates from `mem_map`.
- One stray `decode Value.t` (after the global Json.Codec strip)
restored to `Json.Codec.Value.t` -- it's outside any open scope.
- slack: Json_parse variant stays string; convert Json.Error.t at the
internal api.ml boundary before wrapping.
- meross: Protocol.decode wraps Json.Error.t -> string at boundary.
- stix: bundle_of_string, decode_or_fail, encode_or_fail convert at
boundary.
- oauth: classify_token_error converts incoming Json.Error.t at entry.
- Test files adjusted: Json.Error.to_string for raw Json.of_string calls,
plain strings for package-level APIs that already convert.
Warning 69 (unused-field, mutable-never-assigned). Four independent
record fields were flagged as mutable but the code only mutates their
referents in place, never rebinds the record slot itself:
- ocaml-wal/lib/wal.ml: [t.file] (the Eio file resource; methods call
Eio.File.pwrite_all etc., the slot is set once at open time).
- ocaml-block/lib/block.ml: [Memory.state.data] (the backing bytes,
written via Bytes.blit_string; [Bytes.t] is already mutable).
- ocaml-sse/lib/sse.ml: [Parser.t.data_buf] (a Buffer.t, written via
Buffer.add_*; the slot never changes).
- ocaml-zephyr/lib/zephyr.ml: drop [mode : Read | Write] entirely —
set at open-time, read nowhere. The open_read / open_write
constructors already distinguish the two call shapes, so mode
tracking was redundant.
New S3.Http.put_object_file takes a file path + env, computes the
SigV4 payload hash by streaming the file once through Digestif
(64 KB buffer, no heap allocation beyond the buffer), then hands
Requests.Body.of_file the same path so the HTTP layer streams the
second pass straight to the socket. A 200 MB bottle no longer has
to fit in the OCaml heap.
Sigv4.sign grows an optional ?payload_hash so callers that hash
the body out-of-band (streaming signers, aws-chunked, unsigned
payload) can override the string-based default without rewriting
the signing pipeline.
Bottler.Upload.put_both swaps Bos.OS.File.read + put_object for
put_object_file, removing the last heap-holds-entire-body
behaviour in the upload path.
Commit 5fbed21c switched every cram test fixture from Printf to Fmt
without updating the dune stanzas to depend on fmt, so `dune build`
inside the fixtures fails and the cram expected output stopped
matching reality. Add fmt to each executable/library and refresh the
one stale expected block (cascade_cleanup) still showing Printf.
Generate .opam.template files with x-quality-* fields based on
detected package features:
- x-quality-build: has lib/ with .ml files
- x-quality-test: has test/ with .ml files
- x-quality-fuzz: has fuzz/ with .ml files
- x-quality-interop: has test/interop/ directory
- x-quality-cram: has test/*.t/ directories
These fields are picked up by dune's opam generation and will be
checked by merlint E910 for consistency.
Also: add fmt dep to ocaml-sse/lib/dune (Fmt.pf used without dep).
Standard vocabulary based on crates.io categories, erratique/opam
conventions, and monorepo domain coverage:
Org: org:blacksun
Domain: aerospace, codec, crypto, network, storage, git, merkle
Purpose: cli, test, bench, format, log, system
Protocol: ccsds, uslp, cop1, sdls, sle, atproto, tls, http, json, binary
Cross-cutting: eio, simulation, math, compression
Tags placed in dune-project (package ...) stanzas via (tags ...).
Propagated to .opam files by dune's opam generation.
- compute_proof: None branch was dead code producing a predictable
hash. Now fails explicitly.
- verify_receipts: untrusted TSes (ts_keys returns None) are skipped
instead of rejecting the entire bundle. Fails only if no receipt
from a trusted TS verifies. Matches the cross-signing promise in
the .mli documentation.
code_verifier is now abstract — can only be created via
generate_code_verifier or code_verifier_of_string (which validates
43-128 chars from [A-Za-z0-9._~-]). Prevents passing low-entropy
strings to code_challenge.
code_verifier_to_string provided for session storage. exchange_code
now takes code_verifier instead of string. 2 new validation tests.
parse_token_response is a pure JSON parser — it doesn't produce or
transmit secrets. Removing it would destroy 9 valuable edge-case
tests (invalid JSON, missing fields, token_type validation) with no
security benefit. The transport security boundary is at exchange_code
and refresh_token, not at the parser.
These functions exposed client_secret in cleartext and relied on the
caller to POST over TLS. The secure-by-default API is exchange_code
and refresh_token which handle transport internally.
The form encoding functions remain as internal helpers but are no
longer exported. Tests that called them directly have been removed.
The library now depends on requests and performs the token endpoint
POST itself, enforcing TLS transport for client_secret. The old
exchange_form_body and refresh_form_body are kept as low-level
primitives but documented to prefer the new functions.
ocaml-auth's exchange_code now delegates to Oauth.exchange_code.
parse_token_response now parses the token_type field (RFC 6749 §5.1)
and rejects anything other than "bearer" (case-insensitive). Missing
token_type is accepted as Bearer (GitHub omits it). Previously, MAC
or DPoP tokens were silently accepted and would be sent as Bearer,
creating a confused-deputy situation.
New error variant: Unsupported_token_type of string.
4 new tests: rejects mac, accepts Bearer, case-insensitive, missing.
Add email_verified:bool to Oauth.userinfo. Google OIDC sets it from
the email_verified field; GitHub sets it when /user/emails returns a
verified primary; GitLab and custom default to false. The encoder now
faithfully round-trips the verification status.
ocaml-auth now requires email_verified=true to create a user account.
Sign-in is rejected with a clear error if no verified email is
available, instead of fabricating a fake login@provider address.
userinfo.email is now string option instead of string. For GitHub,
parse_userinfo intentionally returns None — the /user endpoint only
has the public email which is unverified. Add parse_github_emails to
extract the primary verified email from /user/emails (requires
user:email scope).
ocaml-auth's fetch_userinfo now calls /user/emails for GitHub to
populate the verified email. The type change forces all callers to
handle the missing-email case explicitly.
3 new tests for parse_github_emails. Existing tests updated.
The library generated state but provided no validation function,
leaving callers to roll their own (potentially timing-vulnerable)
comparison or skip validation entirely.
Add validate_state using constant-time byte comparison. Update the
section heading and docs to spell out the caller's obligation: store
state in session before redirect, validate on callback, reject on
mismatch. Update module example to show the full generate/validate
flow.
4 new tests: matching, mismatch, empty, and length-mismatch cases.
custom_provider now checks that path_safe(name) does not equal
"github", "google", or "gitlab". A collision would make callback
routes ambiguous, allowing an attacker-controlled custom provider
to intercept authorization callbacks for a legitimate built-in one.
3 new tests: rejects "GitHub" and "Google" slugs, accepts non-colliding.
Introduce Oauth.redirect_uri abstract type with a smart constructor
that enforces HTTPS (or http://localhost per RFC 8252 §7.3) and
rejects fragments (RFC 6749 §3.1.2). The type system now prevents
passing unvalidated, user-controlled strings as the redirect target
(RFC 6749 §10.15).
authorization_url and exchange_form_body now take redirect_uri instead
of string. All callers updated. 6 new tests cover the validation rules.
The fuzz test interpolated raw bytes into JSON strings via Fmt.str,
producing malformed JSON for inputs containing quotes or backslashes.
The test then silently accepted parse errors, hiding the breakage.
Replace string interpolation with Jsont encoding, restrict fuzzed
input to printable ASCII for lossless roundtrips, and require parse
success (only empty access_token may fail).
Add custom_provider smart constructor that validates all endpoint URLs
use HTTPS, as required by RFC 6749 for authorization and token
endpoints. Document TLS requirements on exchange_form_body and
refresh_form_body since both transmit client_secret in cleartext.
4 new tests verify rejection of http:// URLs and acceptance of https://.
The dune-project claimed PKCE support but no implementation existed.
Add code_verifier generation, S256/Plain code_challenge computation,
and integrate into authorization_url and exchange_form_body via
optional parameters. Verified against RFC 7636 Appendix B test vector.
New API: generate_code_verifier, code_challenge, challenge_method type.
Updated: authorization_url and exchange_form_body now accept unit arg
with optional PKCE params. All downstream callers in ocaml-auth updated.
10 unit tests + 2 fuzz tests added. Dependencies: digestif, base64.
Irmin:
- MST codec keyed by Atp.Cid.t (removed Hash↔CID conversion layer)
- Backend.{Memory,Disk}.create_cid — CID-native backends
- Proof.encode_cbor / decode_cbor — CBOR serialization for COSE receipts
- pds_interop: trivial passthrough (no conversion needed)
- 72 tests pass
SCITT:
- Receipt vds (395) in protected header per COSE Receipts spec
- Receipt vdp (396) in unprotected header for proof data
- RFC 9162 VDS: O(1) amortized append, RFC-compliant verify_inclusion
with test vectors from the spec
- MST VDS: Irmin.Proof.Mst.produce at registration, encode_cbor into
receipt, decode + Irmin.Proof.Mst.verify for fully offline verification
- Leaf hash authentication binds proof to specific statement
- 34 main + 17 ATP = 51 tests, all pass
Sigstore:
- Certificate chain validation against Fulcio root CA
- Rekor entry binding (body/log_index/integrated_time comparison)
- Hash algorithm from bundle (not hardcoded SHA256)
- 58 tests pass
Auth:
- Per-provider callback URLs (/auth/<slug>/callback)
- provider_name (raw, for DB) vs provider_slug (URL-safe, for routes)
- Token exchange includes grant_type=authorization_code
- No credential leakage in logs or error responses
Respond:
- HEAD responses suppress body per RFC 9110 §9.3.2
OAuth:
- Provider variant type (Github | Google | Gitlab | Custom)
- Per-provider userinfo JSON schemas (no field guessing)