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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ jobs:
run: npm run build
- name: test:unit
run: npm run test:unit
- name: test:incoming-helpers
run: npm run test:incoming-helpers
- name: test:ci-lifecycle
run: npm run test:ci-lifecycle
integration:
Expand Down
465 changes: 453 additions & 12 deletions docs/incoming-migration.md

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions docs/reuse-assessment.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,30 @@ moderation, credential lifetime and production qualification.
5. **Email coverage:** sending login OTP through SMTP does not prove every XRPC
email challenge is delivered through the same transport.

## Incoming public mechanism provenance

For maintainers evaluating incoming migration, the reference PDS 0.5.36
implementation exposes unauthenticated DID-keyed repository-key reservation and
forwards activation to Entryway when configured. Its repository export captures a
root before paging blocks; import applies a verified diff, and missing-blob
pagination operates on a mutable set. These implementation observations do not
establish source finality or a destination write barrier. The managed standalone
source image is separately pinned to PDS 0.5.34; inspect its version-specific
behavior rather than assuming full equivalence. The synthetic migration fixture
and repository inventory comparison are reuse candidates, not public end-to-end
qualification or proof of a historical source signature.

One bounded committed qualification run observed a real stock source CAR
verification, a nonempty snapshot, two export requests started before a write was
dispatched (request ordering only, not server-side overlap), an
Entryway-hosted inactive destination accepting a streamed import, and sequential
same-DID reservation with placement isolation. That run is bounded by its own
committed byte and phase budgets and does not establish finality, blob pagination
or an activation barrier. C4 therefore stays blocked, while C1, C2, C3 and C5 are
accepted contracts whose callables the downstream plans must still produce; see the
contract gates in
[incoming migration](incoming-migration.md#contract-qualification-gates-for-implementers).

## Custody design reference

The approved model follows [Rust Entryway in atproto-crates](https://tangled.org/ngerakines.me/atproto-crates)
Expand Down
79 changes: 76 additions & 3 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ and requires no host dependency install:
| `./tests/local.sh resilience-profiles` | Run all three fresh deployment profiles and require complete bounded receipts; see [profile contract](deployment-profiles.md) |
| `./tests/local.sh contracts` | Run imported contract scenarios in the test container after up |
| `./tests/local.sh browser` | Run browser scenarios after up |
| `./tests/local.sh incoming-public-mechanisms` | Bounded stock public-mechanism contract test after up: standalone signup, Mailpit, PLC signing, inactive destination and reservation; 12 offline plus 5 live cases, not a migration qualification |
| `./tests/local.sh migration` | Fresh synthetic external-source migration, including two Entryway restarts |
| `./tests/local.sh migration-resume` | Resume a reviewed interrupted fixture; inspect state before using |
| `./tests/local.sh reverify` | Check an already completed migration fixture |
Expand Down Expand Up @@ -77,6 +78,60 @@ Review `resilience.json` and per-phase reports if interrupted. See
| `SANDBOX_PROJECT` | Explicit owned project name |
| `ACCEPTANCE_REPORT_DIR` | Report directory override |

For operators running browser scenarios or the public-mechanism qualification phase,
the controller refreshes and validates sandbox access and writes a private allowlisted
`incoming-access.json` in the owned sandbox. It mounts that file read-only at
`/run/incoming-access.json` in the browser container and sets `INCOMING_ACCESS_PATH`.
The qualification phase additionally sets `INCOMING_PROJECT`; ordinary browser runs
do not. `all` and `fresh` inherit the projection through the browser phase; they do
not run the qualification phase.

`incoming-public-mechanisms` runs the contract test directly in the browser container
with `node --test`, not a Playwright spec:

```sh
./tests/local.sh incoming-public-mechanisms
```

After `up`, that one command runs every case in
`tests/contracts/incoming-public-mechanisms.test.mjs`. The live cases create a unique
account on the standalone PDS, request a PLC confirmation through its public API,
read its recipient-bound message from Mailpit, use the token for public signing, and
observe the inactive destination and reservation behavior. The controller applies the
repository-owned SMTP overlay to pds3 after startup.

Case counts and what they mean. One live-gated test runs two further subcases, so a
full opt-in run reports more cases than the file declares. The file declares fifteen
top-level tests: twelve offline and three live-gated. A full opt-in run reports 17
cases, which is 12 offline plus 5 live. Only those 5 live cases form the public
qualification.

`tests/local.sh incoming-public-mechanisms` always exports
`INCOMING_PUBLIC_MECHANISMS=1` for the container, so unsetting the variable does not
turn that command into an offline run. To run only the offline cases natively, use:

```sh
env -u INCOMING_PUBLIC_MECHANISMS -u INCOMING_ACCESS_PATH node --test \
tests/contracts/incoming-public-mechanisms.test.mjs
```

That run reports the three live-gated tests skipped; those skips are not
qualification. At the time of writing, that native run reports 12 passed and 3
skipped, which is the offline subset only. A live count is a claim about a sandbox run
and must not be reported from this document.

The offline cases cover the managed request boundary and phase budgets. They do not
prove any public behavior, and no Entryway endpoint or route implements this
migration: the contract test consumes stock PDS APIs, not Entryway handlers.

The preflight verifies the signed PLC operation against the source's observed
rotation keys. The remaining contracts are still open in the
[contract gates](incoming-migration.md#contract-qualification-gates-for-implementers):
C4 finality and activation-race closure stay BLOCKED; C1, C2, C3 and C5 are accepted
contracts whose callables the downstream plans must still produce.

Use the owned-project cleanup procedure below, including after failures.

Runtime configuration uses `SERVICE_CONFIG_PATH` (default `./.runtime/config.json`).
`DATABASE_BACKEND` selects SQLite or PostgreSQL and `DEPLOYMENT_MODE` selects the
single-node or multi-node profile. SQLite uses `account-authority.sqlite` under
Expand Down Expand Up @@ -254,13 +309,31 @@ profile controllers are not filtered a second time. Raw reports are never upload
private reports locally for detailed failures. Lifecycle fixtures use fake Docker
natively; they do not establish hosted-runner compatibility.

### Development: native incoming helper checks

Contributors changing incoming test infrastructure run the stock mail poller,
managed I/O helper and access-projection tests explicitly:

```sh
npm run test:incoming-helpers
```

This script runs `tests/support/helpers/stock-plc-mailpit.test.mjs`,
`tests/support/helpers/incoming-public-io.test.mjs` and
`tests/atmosphere/project-incoming-access.test.mjs`. Native CI runs the script in
its own step. Contract discovery does not include these helper files, and the
live stock-mechanism suite is not part of the native unit allowlist.

## Feature tests and operational evidence

Contract discovery executes both `tests/contracts/*.test.mjs` and recursively
co-located `src/features/**/*.test.mjs`, once and sorted, with a shared JUnit report.
Run browser before contracts in a fresh project: real contracts require the
verified account created by the browser journey. Pure diagnostic unit runs use
`npm run test:unit` natively and do not need that fixture.
It does not discover helper tests under `tests/support`, so run those explicitly with
`node --test` when a helper changes. See the rule in
`tests/support/discover-contracts.mjs`. Run browser before contracts in a fresh
project: real contracts require the verified account created by the browser journey.
Pure diagnostic unit runs use `npm run test:unit` natively and do not need that
fixture.
Browser email-proof tests must obtain the recipient's visible code from Mailpit
message content; reading the database outbox's full token hides missing challenge
context. Verify the hidden selector is retained and that wrong codes or selectors
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"changeset": "changeset",
"changeset:check": "changeset status",
"version-packages": "changeset version && npm install --package-lock-only --ignore-scripts --offline --no-audit --no-fund",
"test:incoming-helpers": "node --test tests/support/helpers/stock-plc-mailpit.test.mjs tests/support/helpers/incoming-public-io.test.mjs tests/atmosphere/project-incoming-access.test.mjs",
"test:ci-lifecycle": "python3 tests/support/ci-lifecycle.test.py"
},
"dependencies": {
Expand Down
112 changes: 112 additions & 0 deletions tests/atmosphere/project-incoming-access.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";

function origin(value) {
if (typeof value !== "string") throw new Error("Invalid HTTPS origin");
let parsed;
try {
parsed = new URL(value);
} catch {
throw new Error("Invalid HTTPS origin");
}
if (
parsed.protocol !== "https:" ||
parsed.username ||
parsed.password ||
parsed.pathname !== "/" ||
parsed.search ||
parsed.hash ||
(value !== parsed.origin && value !== `${parsed.origin}/`)
) {
throw new Error("Invalid HTTPS origin");
}
return parsed.origin;
}

export function projectIncomingAccess(access, project, routes) {
if (
typeof project !== "string" ||
!/^hypercerts-entryway(-[a-z0-9_-]+)?$/.test(project) ||
access?.schemaVersion !== 1 ||
access.project !== project ||
access.network?.internal !== true ||
access.network?.physicalName !== `${project}_atmosinabox` ||
access.observations?.configuration !== "current" ||
access.trust?.certificateExists !== true ||
!Array.isArray(access.endpoints)
) {
throw new Error("Invalid or stale private sandbox access");
}
if (
!Array.isArray(routes?.destinationUrls) ||
routes.destinationUrls.length !== 2
) {
throw new Error("Expected two configured destination origins");
}
const expected = [
["pds", "pds3", origin(routes.sourceUrl)],
["application", "entryway", origin(routes.entrywayUrl)],
["application", "pds1", origin(routes.destinationUrls[0])],
["application", "pds2", origin(routes.destinationUrls[1])],
["plc", "plc", origin(routes.plcUrl)],
["application", "mail-capture", origin(routes.mailpitUrl)],
];
if (new Set(expected.map((item) => item[2])).size !== expected.length) {
throw new Error("Configured origins must be distinct");
}
for (const [serviceType, service, expectedOrigin] of expected) {
const hostname = new URL(expectedOrigin).hostname;
const matching = access.endpoints.filter(
(endpoint) => endpoint?.hostname === hostname,
);
if (
matching.length !== 1 ||
matching[0].serviceType !== serviceType ||
matching[0].service !== service ||
matching[0].kind !== "exact" ||
origin(matching[0].url) !== expectedOrigin
) {
throw new Error("Missing, ambiguous or inconsistent sandbox endpoint");
}
}
return {
schemaVersion: 1,
project,
sourceUrl: expected[0][2],
entrywayUrl: expected[1][2],
destinationUrls: [expected[2][2], expected[3][2]],
plcUrl: expected[4][2],
mailpitUrl: expected[5][2],
};
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const [
accessPath,
project,
sourceUrl,
entrywayUrl,
firstDestination,
secondDestination,
plcUrl,
mailpitUrl,
...extra
] = process.argv.slice(2);
if (!mailpitUrl || extra.length) {
throw new Error(
"usage: project-incoming-access.mjs <access.json> <project> <source-origin> <entryway-origin> <destination1-origin> <destination2-origin> <plc-origin> <mailpit-origin>",
);
}
const access = JSON.parse(await readFile(accessPath, "utf8"));
const projection = projectIncomingAccess(access, project, {
sourceUrl,
entrywayUrl,
destinationUrls: [firstDestination, secondDestination],
plcUrl,
mailpitUrl,
});
process.stdout.write(`${JSON.stringify(projection)}\n`);
}
Loading