Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changes/016-http-client-byte-level-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
group: platform
component: Framework
impact: compatible
category: Added
---

## Summary

`Http.Client` can now send PUT, HEAD, DELETE, POST and PATCH requests with a raw byte body and read the status code and headers of every response, including 4xx and 5xx. Use `Http.Client.sendSecure method request body` for HTTPS services and `Http.Client.Internal.sendRaw` for trusted loopback endpoints. `Http.getSecure` and `getRaw` keep working exactly as before.

Existing applications do not need code changes. To verify locally, run `./dev test "Http.Client" nhcore-test-core` and confirm the `sendRaw` and `sendSecure` examples pass.
12 changes: 12 additions & 0 deletions .changes/017-s3-blob-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
group: platform
component: Framework
impact: compatible
category: Added
---

## Summary

File uploads can now be stored in any S3-compatible object store (AWS S3, Cloudflare R2, Backblaze B2, MinIO, RustFS) with the new `Service.FileUpload.BlobStore.S3` backend. Build it in `App.hs` with `S3.createBlobStore S3Config { endpoint, bucket, region, accessKeyId, secretAccessKey }`, reading the values from your own `Config.hs`, and pass the result to `Application.withFileUpload` exactly like the local backend. Applications on the local filesystem backend are not affected.

To verify locally, run `./dev test "BlobStore.S3" nhcore-test-service`; the suite signs requests against the published AWS examples and exercises the backend against an in-process fake S3 server.
12 changes: 12 additions & 0 deletions .changes/021-crypto-sha256-hmac-hex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
group: platform
component: Framework
impact: compatible
category: Added
---

## Summary

`Crypto` now offers the raw building blocks behind signatures: `Crypto.sha256` (32-byte digest), `Crypto.hmacSha256 key message` (raw HMAC that can be chained, any key length) and `Crypto.toHex` (lowercase hexadecimal). Use them when a protocol tells you exactly which bytes to hash or sign; keep using `Crypto.signWith` and `Crypto.verifyWith` for webhook secrets, which are unchanged.

Existing applications do not need code changes. To verify locally, run `./dev test "Crypto" nhcore-test-core` and confirm the `sha256`, `hmacSha256` and `toHex` examples pass.
29 changes: 26 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,21 @@ jobs:
with:
name: dist-newstyle
path: dist-newstyle
# Tee the direct per-example output: integration criteria whose locators
# live in nhcore-test-core are proven from this report by the criteria
# gate in test-service. Non-hidden filename: upload-artifact skips
# dotfiles by default.
- name: Run nhcore-test-core
run: LOG_LEVEL=Warn nix develop --command cabal test nhcore-test-core
run: |
set -o pipefail
LOG_LEVEL=Warn nix develop --command cabal test nhcore-test-core \
--test-show-details=direct | tee nhcore-test-core.report
- uses: actions/upload-artifact@v7
with:
name: nhcore-test-core-report
path: nhcore-test-core.report
retention-days: 1
if-no-files-found: error

test-auth:
needs: [build]
Expand Down Expand Up @@ -234,7 +247,8 @@ jobs:
run: LOG_LEVEL=Warn nix develop --command cabal test nhcore-test-integration

test-service:
needs: [build]
# test-core supplies its report for the criteria gate below.
needs: [build, test-core]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
Expand Down Expand Up @@ -277,12 +291,21 @@ jobs:
set -o pipefail
LOG_LEVEL=Warn nix develop --command cabal test nhcore-test-service \
--test-show-details=direct | tee .nhcore-test-service.report
- uses: actions/download-artifact@v8
if: github.event_name == 'pull_request'
with:
name: nhcore-test-core-report
# One report across both suites: a missing core report fails `cat`
# (closed), never a silently service-only gate.
- name: Prove integration criterion selectors and real fixtures
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
POSTGRES_AVAILABLE: "true"
run: ./dev spec-check --criteria-runtime "origin/${BASE_REF}" --report .nhcore-test-service.report
run: |
set -euo pipefail
cat nhcore-test-core.report .nhcore-test-service.report > nhcore-criteria.report
./dev spec-check --criteria-runtime "origin/${BASE_REF}" --report nhcore-criteria.report

test-hurl:
needs: [build]
Expand Down
4 changes: 2 additions & 2 deletions codemap/.doc-ratchet
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# doc-ratchet: counters may DECREASE only (CI-enforced by check.py).
# Source-derived (platform-independent) — see generate.py.
undocumented_modules: 212
undocumented_exports: 560
undocumented_doctest_modules: 315
undocumented_exports: 545
undocumented_doctest_modules: 314
116 changes: 58 additions & 58 deletions codemap/api-hot.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## Task

- `yield :: value -> Task w value` <!-- 596 call sites -->
- `yield :: value -> Task w value` <!-- 605 call sites -->
- `ignoreError :: Task err Unit -> Task w Unit` <!-- 240 call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 224 call sites -->
- `throw :: err -> Task err w` <!-- 211 call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 225 call sites -->
- `throw :: err -> Task err w` <!-- 222 call sites -->
- `fromIO :: IO value -> Task w value` <!-- 130 call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 88 call sites -->
- `when :: Bool -> Task err Unit -> Task err Unit` <!-- 31 call sites -->
- `when :: Bool -> Task err Unit -> Task err Unit` <!-- 33 call sites -->
- `forEach :: (element -> Task err Unit) -> Array element -> Task err Unit` <!-- 30 call sites -->
- `fromFailableIO :: Exception exception => IO result -> Task exception result` <!-- 24 call sites -->
- `map :: (input -> output) -> Task err input -> Task err output` <!-- 23 call sites -->

## Array

- `map :: (a -> b) -> Array a -> Array b` <!-- 101 call sites -->
- `map :: (a -> b) -> Array a -> Array b` <!-- 108 call sites -->
- `>>> map sqrt (fromLinkedList [1,4,9] :: Array Float)` → `Array [1.0,2.0,3.0]`
- `fromLinkedList :: LinkedList a -> Array a` <!-- 92 call sites -->
- `empty :: Array a` <!-- 65 call sites -->
- `fromLinkedList :: LinkedList a -> Array a` <!-- 94 call sites -->
- `empty :: Array a` <!-- 66 call sites -->
- `>>> empty :: Array Int` → `Array []`
- `toLinkedList :: Array a -> LinkedList a` <!-- 59 call sites -->
- `toLinkedList :: Array a -> LinkedList a` <!-- 60 call sites -->
- `>>> toLinkedList (fromLinkedList [3,5,8] :: Array Int)` → `[3,5,8]`
- `isEmpty :: Array a -> Bool` <!-- 37 call sites -->
- `>>> isEmpty empty` → `True` …
Expand All @@ -47,14 +47,14 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

- `fromLinkedList :: LinkedList Char -> Text` <!-- 90 call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 78 call sites -->
- `toBytes :: Text -> Bytes` <!-- 66 call sites -->
- `toBytes :: Text -> Bytes` <!-- 75 call sites -->
- `replace :: Text -> Text -> Text -> Text` <!-- 33 call sites -->
- `isEmpty :: Text -> Bool` <!-- 31 call sites -->
- `joinWith :: Text -> Array Text -> Text` <!-- 30 call sites -->
- `trim :: Text -> Text` <!-- 28 call sites -->
- `fromBytes :: Bytes -> Text` <!-- 27 call sites -->
- `trim :: Text -> Text` <!-- 27 call sites -->
- `joinWith :: Text -> Array Text -> Text` <!-- 26 call sites -->
- `isEmpty :: Text -> Bool` <!-- 25 call sites -->
- `append :: Text -> Text -> Text` <!-- 22 call sites -->
- `toLower :: Text -> Text` <!-- 20 call sites -->
- `append :: Text -> Text -> Text` <!-- 23 call sites -->
- `toLower :: Text -> Text` <!-- 21 call sites -->

## Json

Expand Down Expand Up @@ -116,21 +116,21 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## Bytes

- `unwrap :: Bytes -> ByteString` <!-- 47 call sites -->
- `fromLegacy :: ByteString -> Bytes` <!-- 30 call sites -->
- `toLazyLegacy :: Bytes -> LazyByteString` <!-- 16 call sites -->
- `length :: Bytes -> Int` <!-- 4 call sites -->
- `unwrap :: Bytes -> ByteString` <!-- 50 call sites -->
- `fromLegacy :: ByteString -> Bytes` <!-- 33 call sites -->
- `toLazyLegacy :: Bytes -> LazyByteString` <!-- 17 call sites -->
- `empty :: Bytes` <!-- 5 call sites -->
- `length :: Bytes -> Int` <!-- 5 call sites -->
- `>>> [1, 2, 3] |> Bytes.pack |> Bytes.length` → `3`
- `pack :: [Word8] -> Bytes` <!-- 3 call sites -->
- `toBase64 :: Bytes -> Bytes` <!-- 3 call sites -->
- `dropEnd :: Int -> Bytes -> Bytes` <!-- 2 call sites -->
- `isSuffixOf :: Bytes -> Bytes -> Bool` <!-- 2 call sites -->
- `splitOnce :: Bytes -> Bytes -> Maybe (Bytes, Bytes)` <!-- 2 call sites -->
- `fromLazyLegacy :: LazyByteString -> Bytes` <!-- 1 call sites -->

## Maybe

- `withDefault :: a -> Maybe a -> a` <!-- 56 call sites -->
- `withDefault :: a -> Maybe a -> a` <!-- 59 call sites -->
- `map :: (a -> b) -> Maybe a -> Maybe b` <!-- 14 call sites -->
- `getOrDie :: HasCallStack => Maybe a -> a` <!-- 9 call sites -->
- `andThen :: (a -> Maybe b) -> Maybe a -> Maybe b` <!-- 4 call sites -->
Expand Down Expand Up @@ -169,7 +169,7 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## LinkedList

- `map :: (a -> b) -> LinkedList a -> LinkedList b` <!-- 15 call sites -->
- `map :: (a -> b) -> LinkedList a -> LinkedList b` <!-- 16 call sites -->
- `any :: (a -> Bool) -> LinkedList a -> Bool` <!-- 6 call sites -->
- `filter :: (a -> Bool) -> LinkedList a -> LinkedList a` <!-- 5 call sites -->
- `reverse :: LinkedList a -> LinkedList a` <!-- 5 call sites -->
Expand Down Expand Up @@ -201,6 +201,19 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `race :: Show err => Task err a -> Task err b -> Task err (RaceWinner a b)` <!-- 1 call sites -->
- `waitCatch :: Show err => AsyncTask err result -> Task err2 (Result Text result)` <!-- 1 call sites -->

## Http.Client

- `request :: Request` <!-- 7 call sites -->
- `withUrl :: Text -> Request -> Request` <!-- 7 call sites -->
- `withTimeout :: Int -> Request -> Request` <!-- 6 call sites -->
- `addHeader :: Text -> Text -> Request -> Request` <!-- 4 call sites -->
- `get :: FromJSON response => Request -> Task Error (Response response)` <!-- 3 call sites -->
- `postForm :: FromJSON response => Request -> Array (Text, Text) -> Task Error (Response response)` <!-- 3 call sites -->
- `methodName :: Method -> Text` <!-- 2 call sites -->
- `patch :: (FromJSON response, ToJSON requestBody) => Request -> requestBody -> Task Error (Response response)` <!-- 2 call sites -->
- `post :: (FromJSON response, ToJSON requestBody) => Request -> requestBody -> Task Error (Response response)` <!-- 2 call sites -->
- `put :: (FromJSON response, ToJSON requestBody) => Request -> requestBody -> Task Error (Response response)` <!-- 2 call sites -->

## Path

- `toLinkedList :: Path -> LinkedList Char` <!-- 18 call sites -->
Expand Down Expand Up @@ -237,6 +250,13 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `selectEventBatch :: Var Int64 -> Maybe RelativePosition -> Maybe ReadDirection -> Maybe (Array EntityName) -> Task PostgresStoreError (Session (Array PostgresEventRecord))` <!-- 1 call sites -->
- `selectEventByGlobalPositionSession :: Int64 -> Session (Maybe PostgresEventRecord)` <!-- 1 call sites -->

## DateTime

- `now :: Task w DateTime` <!-- 19 call sites -->
- `toEpochSeconds :: DateTime -> Int64` <!-- 11 call sites -->
- `fromEpochSeconds :: Int64 -> DateTime` <!-- 6 call sites -->
- `addSeconds :: Int64 -> DateTime -> DateTime` <!-- 1 call sites -->

## Stream

- `fromArray :: Array value -> Task error (Stream value)` <!-- 7 call sites -->
Expand All @@ -247,26 +267,6 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `consumeMaybe :: (accumulator -> value -> Task Text accumulator) -> accumulator -> Stream value -> Task Text (Maybe accumulator)` <!-- 2 call sites -->
- `end :: Stream value -> Task error Unit` <!-- 2 call sites -->

## DateTime

- `now :: Task w DateTime` <!-- 18 call sites -->
- `toEpochSeconds :: DateTime -> Int64` <!-- 10 call sites -->
- `fromEpochSeconds :: Int64 -> DateTime` <!-- 6 call sites -->
- `addSeconds :: Int64 -> DateTime -> DateTime` <!-- 1 call sites -->

## Http.Client

- `request :: Request` <!-- 6 call sites -->
- `withUrl :: Text -> Request -> Request` <!-- 6 call sites -->
- `withTimeout :: Int -> Request -> Request` <!-- 5 call sites -->
- `get :: FromJSON response => Request -> Task Error (Response response)` <!-- 3 call sites -->
- `postForm :: FromJSON response => Request -> Array (Text, Text) -> Task Error (Response response)` <!-- 3 call sites -->
- `addHeader :: Text -> Text -> Request -> Request` <!-- 2 call sites -->
- `patch :: (FromJSON response, ToJSON requestBody) => Request -> requestBody -> Task Error (Response response)` <!-- 2 call sites -->
- `post :: (FromJSON response, ToJSON requestBody) => Request -> requestBody -> Task Error (Response response)` <!-- 2 call sites -->
- `put :: (FromJSON response, ToJSON requestBody) => Request -> requestBody -> Task Error (Response response)` <!-- 2 call sites -->
- `delete :: FromJSON response => Request -> Task Error (Response response)` <!-- 1 call sites -->

## Service.Transport.Mcp.JsonRpc

- `errorResponse :: Maybe Value -> JsonRpcError -> JsonRpcResponse` <!-- 12 call sites -->
Expand All @@ -283,7 +283,7 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `fmap :: Functor f => (a -> b) -> f a -> f b` <!-- 17 call sites -->
- `map :: Mappable mappable => (typeA -> typeB) -> mappable typeA -> mappable typeB` <!-- 16 call sites -->

*cut: 103 more modules (AtomicVar (26), Integration.Http (26), Decider (21), Result (21), Lock (20), …) — full surface: codemap/signatures/*
*cut: 106 more modules (AtomicVar (26), Integration.Http (26), Decider (21), Result (21), Lock (20), …) — full surface: codemap/signatures/*

---

Expand All @@ -294,35 +294,35 @@ when WRITING TESTS; they are not feature-code frequency signal.

## Task

- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 824 test call sites -->
- `yield :: value -> Task w value` <!-- 466 test call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 283 test call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 827 test call sites -->
- `yield :: value -> Task w value` <!-- 476 test call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 303 test call sites -->
- `mapArray :: (element -> Task err output) -> Array element -> Task err (Array output)` <!-- 126 test call sites -->
- `throw :: err -> Task err w` <!-- 118 test call sites -->
- `fromIO :: IO value -> Task w value` <!-- 97 test call sites -->
- `andThen :: (input -> Task err output) -> Task err input -> Task err output` <!-- 92 test call sites -->
- `fromIO :: IO value -> Task w value` <!-- 90 test call sites -->
- `map :: (input -> output) -> Task err input -> Task err output` <!-- 83 test call sites -->

## Array

- `length :: Array a -> Int` <!-- 265 test call sites -->
- `empty :: Array a` <!-- 221 test call sites -->
- `fromLinkedList :: LinkedList a -> Array a` <!-- 220 test call sites -->
- `map :: (a -> b) -> Array a -> Array b` <!-- 136 test call sites -->
- `get :: Int -> Array a -> Maybe a` <!-- 81 test call sites -->
- `empty :: Array a` <!-- 222 test call sites -->
- `fromLinkedList :: LinkedList a -> Array a` <!-- 222 test call sites -->
- `map :: (a -> b) -> Array a -> Array b` <!-- 139 test call sites -->
- `get :: Int -> Array a -> Maybe a` <!-- 82 test call sites -->
- `wrap :: a -> Array a` <!-- 56 test call sites -->
- `initialize :: Int -> (Int -> a) -> Array a` <!-- 47 test call sites -->
- `contains :: Eq value => value -> Array value -> Bool` <!-- 41 test call sites -->

## Text

- `contains :: Text -> Text -> Bool` <!-- 436 test call sites -->
- `toBytes :: Text -> Bytes` <!-- 89 test call sites -->
- `contains :: Text -> Text -> Bool` <!-- 439 test call sites -->
- `toBytes :: Text -> Bytes` <!-- 117 test call sites -->
- `fromLinkedList :: LinkedList Char -> Text` <!-- 58 test call sites -->
- `length :: Text -> Int` <!-- 41 test call sites -->
- `repeat :: Int -> Text -> Text` <!-- 35 test call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 29 test call sites -->
- `startsWith :: Text -> Text -> Bool` <!-- 18 test call sites -->
- `startsWith :: Text -> Text -> Bool` <!-- 20 test call sites -->
- `all :: (Char -> Bool) -> Text -> Bool` <!-- 10 test call sites -->

## Parser
Expand Down Expand Up @@ -360,13 +360,13 @@ when WRITING TESTS; they are not feature-code frequency signal.

## ConcurrentVar

- `containing :: value -> Task w (ConcurrentVar value)` <!-- 125 test call sites -->
- `modify :: (value -> value) -> ConcurrentVar value -> Task w Unit` <!-- 120 test call sites -->
- `peek :: ConcurrentVar value -> Task w value` <!-- 102 test call sites -->
- `containing :: value -> Task w (ConcurrentVar value)` <!-- 127 test call sites -->
- `modify :: (value -> value) -> ConcurrentVar value -> Task w Unit` <!-- 121 test call sites -->
- `peek :: ConcurrentVar value -> Task w value` <!-- 107 test call sites -->
- `get :: ConcurrentVar value -> Task w value` <!-- 39 test call sites -->
- `set :: value -> ConcurrentVar value -> Task w ()` <!-- 15 test call sites -->
- `new :: forall value w. Task w (ConcurrentVar value)` <!-- 14 test call sites -->
- `swap :: value -> ConcurrentVar value -> Task w value` <!-- 7 test call sites -->
- `swap :: value -> ConcurrentVar value -> Task w value` <!-- 8 test call sites -->
- `modifyReturning :: (value -> Task Never (value, a)) -> ConcurrentVar value -> Task w a` <!-- 6 test call sites -->

## Uuid
Expand All @@ -377,4 +377,4 @@ when WRITING TESTS; they are not feature-code frequency signal.
- `generateV5 :: Uuid -> Text -> Uuid` <!-- 19 test call sites -->
- `fromText :: Text -> Maybe Uuid` <!-- 3 test call sites -->

*cut: 138 more modules (Map (257), Service.Application (250), Set (233), AsyncTask (223), Stream (212), …) — full surface: codemap/signatures/*
*cut: 139 more modules (Map (257), Service.Application (250), Set (233), AsyncTask (223), Stream (212), …) — full surface: codemap/signatures/*
35 changes: 34 additions & 1 deletion codemap/phrasebook.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ Array [0]
[]
```

### Crypto

```haskell
>>> Bytes.pack [0, 15, 255] |> Crypto.toHex
"000fff"
```

```haskell
>>> Text.toBytes "abc" |> Crypto.sha256 |> Crypto.toHex
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
```

### Float

```haskell
Expand Down Expand Up @@ -194,6 +206,27 @@ Set (fromList [1,2,3])
"Cart"
```

## file-upload

### Service.FileUpload.BlobStore.S3

```haskell
>>> isDnsBucketName "my-bucket"
True
```

```haskell
>>> isDnsBucketName "My_Bucket"
False
```

### Service.FileUpload.BlobStore.S3.SigV4

```haskell
>>> Text.toBytes "" |> sha256Hex
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
```

## postgres-infra

### Service.Infra.Postgres.SslMode
Expand Down Expand Up @@ -237,4 +270,4 @@ True

---

*32 example sessions · modules with ZERO doctests: 315 (the documentation backlog — see codemap/.doc-ratchet)*
*37 example sessions · modules with ZERO doctests: 314 (the documentation backlog — see codemap/.doc-ratchet)*
Loading
Loading