Skip to content
Merged
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
27 changes: 7 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,14 @@ on:
pull_request:

jobs:
go-fmt-vet-build:
name: cabi (gofmt, go vet, go test, build)
runs-on: ubuntu-latest
defaults:
run:
working-directory: cabi
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: cabi/go.mod
- run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: go test ./...
- run: go build -buildmode=c-shared -o libmacula.so .

php-test:
name: php (lint, composer validate, phpunit against in-process stations)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: cabi/go.mod
go-version: "1.27"
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
Expand All @@ -40,8 +24,11 @@ jobs:
php -l "$f"
done
- run: composer install --no-interaction --prefer-dist
# libmacula.so and build/teststation, two in-process macula 12
# stations the offline suite runs against. tests/live dials the real
# fleet and never runs here: `composer test:live`, by hand.
# The released libmacula for abi/MACULA_GO_REF, checked against its
# SHA256SUMS and attestation, and macula-go's teststation at that tag:
# two in-process stations the offline suite runs against. tests/live
# dials the real fleet and never runs here: `composer test:live`, by hand.
- run: composer build
env:
GH_TOKEN: ${{ github.token }}
- run: vendor/bin/phpunit
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@
/composer.lock
/.phpunit.cache/
/build/
/cabi/libmacula.so
/cabi/libmacula.h
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,37 @@ to v0.3.4 are described in their annotated git tag messages.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.0] - 2026-10-05

### Changed

- macula-php loads macula-go's **released** libmacula, the tag in
`abi/MACULA_GO_REF`, instead of its own `cabi/` over macula-go v0.12.0,
which is gone. `composer build` downloads the library for this platform,
checks it against the release's `SHA256SUMS` and build attestation, and
refuses a release whose `macula.h` is not `abi/macula.h`; `Binding` refuses
a library of another ABI version. The test suite runs macula-go's own
teststation at the same tag. This brings the macula 13 wire: handshake v5
bound to the TLS session, and macula 13 advertisements that name a
provider's KEM key, which v0.12.0 refused as malformed ("no trusted
provider").
- Errors cross as libmacula's JSON errors: `MaculaException::$kind` is the
library's error kind ("timeout", "not_found", "no_provider", "refused", ...),
and `ProviderError`, `RelayError`, `NotSharedError` and
`ContentUnavailableError` are thrown for their kinds as before.
- `NodeKey::loadOrCreate` is libmacula's: a key file that exists but cannot be
read is an error, never a reason to make a new key.

### Added

- End-to-end sealing: `Pool::call` and `Pool::openStream` take
`confidential: Confidentiality::Preferred|Required`, sealed whenever the
provider's advertisement names a KEM key; `Required` fails with a
`ConfidentialityError` (`reason`, `named`, `found`) rather than go in the
clear. `Pool::serve` and `Pool::serveStream` take `confidential:` too (`Off`
as well), and `Pool::connect` takes `kemAdvertise:` to name this node's KEM
key in its advertisements. A served `Request` has `sealed`.

## [0.6.0] - 2026-09-26

### Added
Expand Down
69 changes: 37 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,33 @@

---

> **Status, 2026-09-26:** on the **macula 12** wire (post-quantum: ML-DSA-87
> identities, as the ML-DSA-87 + RSA-PSS-4096 composite in pq_hybrid, the
> fleet's profile; ML-KEM hybrid key exchange; signed requests), over macula-go
> v0.12.0's pool. Calls and streams by direct dial, serving (under an org or in
> a node's own namespace), publish/subscribe, the DHT and node-served content
> are tested against in-process macula 12 stations on every `composer test`.
> UCAN-gated calls are not here yet; see [Not yet
> implemented](#not-yet-implemented). Releases before 0.5.0 speak the retired
> 10.x wire and cannot reach the current fleet.
> **Status, 2026-10-05:** over macula-go's **released** libmacula (the tag in
> `abi/MACULA_GO_REF`, ABI 1), so on the macula 13 wire: handshake v5 bound to
> the TLS session, end-to-end sealed calls and streams to a provider that
> advertises a KEM key (`Confidentiality`), post-quantum identities (ML-DSA-87,
> as the ML-DSA-87 + RSA-PSS-4096 composite in pq_hybrid, the fleet's profile)
> and ML-KEM hybrid key exchange. Calls and streams by direct dial, serving
> (under an org or in a node's own namespace), publish/subscribe, the DHT and
> node-served content are tested against in-process stations on every
> `composer test`. UCAN-gated calls are not in the PHP API yet; see [Not yet
> implemented](#not-yet-implemented). Releases before 0.7.0 bind macula-go
> v0.12.0 and cannot call a provider that advertises a KEM key.

## What is this?

A PHP SDK for the Macula mesh: a node's key, a pool of links to stations it
pins by node_id, calls and streams that reach a provider by direct dial,
serving procedures, publish/subscribe, node-served content and the DHT. It is
an FFI binding over [macula-go](https://github.com/macula-io/macula-go): the Go
SDK is compiled into one shared library, `libmacula.so`, which PHP loads with
`ext-ffi`, rather than a third implementation of QUIC, TLS 1.3 with a hybrid
SDK's released shared library, libmacula, which PHP loads with `ext-ffi`,
rather than a third implementation of QUIC, TLS 1.3 with a hybrid
post-quantum key exchange, deterministic CBOR and signed frames.

## Quick start

```bash
composer require macula-io/macula-php
cd vendor/macula-io/macula-php && composer build # compiles cabi/ into libmacula.so
cd vendor/macula-io/macula-php && composer build # fetches and checks libmacula
```

A node needs a station to link to, **pinned by its node_id**, and the key of
Expand Down Expand Up @@ -125,7 +127,8 @@ compatibility layer.
|---|---|---|---|
| Node keys (`NodeKey`) | ✅ | ✅ | `pq_hybrid` (the fleet's) or `pq_pure`; key files readable by the owner only; `sign` and `verify`, pq_hybrid checked against the LAMPS draft's own vector and cross-verified with macula 12.7.0 |
| Pool of station links (`Pool::connect`) | ✅ | ✅ | Seeds pinned by node_id; realm keys pinned; links redialed with subscriptions and served procedures replayed |
| Calls by direct dial (`call`, `providers`) | ✅ | ✅ | Errors arrive as `ProviderError` / `RelayError` |
| Calls by direct dial (`call`, `providers`) | ✅ | ✅ | Errors arrive as `ProviderError` / `RelayError`; every other failure is a `MaculaException` whose `kind` is the library's error kind |
| End-to-end sealing (`confidential:`, `kemAdvertise:`) | ✅ | ✅ | A call or stream is sealed whenever the provider's advertisement names a KEM key; `Confidentiality::Required` fails with `ConfidentialityError` rather than go in the clear; a served `Request` says whether it came `sealed` |
| A node's own namespace (`ownProcedure`) | ✅ | ✅ | `~<node_id>/<name>`: served and called with no org and no realm key; the node's signature authorizes it |
| Streams (`openStream`, `serveStream`) | ✅ | ✅ | Server, client and bidi; a QUIC stream per session, released on every path |
| Publish/subscribe | ✅ | ✅ | Signed publications, delivered once across links |
Expand Down Expand Up @@ -168,38 +171,40 @@ bytes, and come back as hex.
## Architecture

```
src/ (PHP API) ── src/Binding.php (ext-ffi) ── cabi/ (Go, libmacula.so) ── macula-go pool
src/ (PHP API) ── src/Binding.php (ext-ffi) ── libmacula (macula-go's C ABI) ── macula-go pool
```

`cabi/` exports C functions over macula-go's `pool` (and `stationlink`
streams). Every Go value crosses as a `runtime/cgo.Handle`; payloads cross as
JSON. Every call that does network I/O blocks the calling PHP thread. PHP's FFI
cannot take a callback on a Go thread, so what arrives on its own (a
subscription's events, a served procedure's calls, a streaming procedure's
sessions) waits in a bounded inbox in `cabi/inbox.go` until a `*_next`
function takes it, with a wait of its own.
libmacula is macula-go's C ABI (`abi/macula.h`, its contract in macula-go's
`cabi/CONTRACT.md`), the same library macula-py, Macula .NET and macula-ts
load. `composer build` downloads it for the tag in `abi/MACULA_GO_REF`, checks
it against the release's `SHA256SUMS` and build attestation, and refuses a
release whose `macula.h` is not `abi/macula.h`; `Binding` refuses a library of
another ABI version. Payloads cross as JSON. Every call that does network I/O
blocks the calling PHP thread. What arrives on its own (a subscription's
events, a served procedure's calls, a streaming procedure's sessions) waits in
a bounded inbox in the library until a `*_next` function takes it.

## Not yet implemented

- **UCAN-gated calls and serving.** macula 12 uses post-quantum UCANs
(macula-go#2). Calls carry no token yet, and a gated procedure cannot be
served.
- **UCAN-gated calls and serving.** libmacula has them
(`macula_pool_call_opts` with a "ucan", `macula_pool_serve_opts` with a
policy); the PHP API does not expose them yet.
- **The seal report** (`report`, `macula_stream_report`): not in the PHP API.

## Testing

```bash
composer install
composer build # libmacula.so and build/teststation
composer build # libmacula into build/native, and build/teststation
composer test # the offline suite
composer test:live # one live station, see below
```

`composer test` runs `tests/PoolTest.php` against `cabi/cmd/teststation`, a
helper that runs two in-process macula 12 stations (macula-go's `teststation`)
sharing a DHT, with a test realm that admits the test's provider nodes. It
`composer test` runs `tests/PoolTest.php` against macula-go's teststation at
the same tag, which runs two in-process stations sharing a DHT, with a test realm that admits the test's provider nodes. It
exercises keys, calls by direct dial and their errors, providers, server and
client streams and a provider that aborts one (and that no stream is left
unreleased), pubsub, node-served content and the DHT, through the real library.
unreleased), a sealed call and a required call refused in the clear, pubsub, node-served content and the DHT, through the real library.
A provider a test calls runs in a PHP process of its own
(`tests/fixtures/provider.php`). No network is needed. The suite needs PHP ≥
8.3 (PHPUnit 12's floor); the library itself runs on PHP ≥ 8.1.
Expand All @@ -222,8 +227,8 @@ key generated for the run and never saved, it reads the DHT, calls

- PHP ≥ 8.1 with `ext-ffi`. `ext-ffi` is not always enabled in distro PHP
builds: check `php -m | grep FFI`; PHP is built with it by `--with-ffi`.
- Go ≥ 1.26 and a C compiler (cgo), to build `cabi/`. `composer build` is the
only Go command you run; you never write Go.
- `gh` with a `GH_TOKEN` and `sha256sum`, for `composer build` to fetch and
check libmacula. Go 1.27, only for the test suite's teststation.
- Composer.

## Sibling SDKs
Expand All @@ -232,7 +237,7 @@ key generated for the run and never saved, it reads the DHT, calls
|---|---|
| [macula](https://github.com/macula-io/macula) | The reference SDK (Erlang/OTP) |
| [macula-go](https://github.com/macula-io/macula-go) | Go port, and what this SDK binds |
| [macula-ts](https://github.com/macula-io/macula-ts) | FFI binding over macula-go, for Node.js; this SDK's `cabi/` follows its pool model |
| [macula-ts](https://github.com/macula-io/macula-ts) | FFI binding over the same libmacula, for Node.js |
| [macula-rust](https://github.com/macula-io/macula-rust) | Native reimplementation (quinn, pure Rust) |
| [macula-station](https://github.com/macula-io/macula-station) | The station: DHT, SWIM, routing, peering |
| [macula-realm](https://github.com/macula-io/macula-realm) | Managed-realm identity + certificate authority |
Expand Down
1 change: 1 addition & 0 deletions abi/MACULA_GO_REF
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.22.0 dc23a8e80401c74c20f74a934c4bdf847dd393c6
Loading
Loading