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
5 changes: 5 additions & 0 deletions .changeset/probe-lexicons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@getcirrus/space-conformance": patch
---

Probe spaces now use published, resolvable lexicons (`earth.cirrus.check.space` / `.note` / `.withblob`) instead of the unpublished `app.bsky.group` type and unresolvable `test.conformance.*` collections, so the suite works against implementations that resolve lexicons dynamically and validate strictly — not only against lenient targets.
8 changes: 5 additions & 3 deletions apps/check/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -751,13 +751,15 @@ function ConfirmSpacesView(props: {
<ul class="space-y-2 pl-4">
<li>
<span class="text-ink">·</span> Create probe spaces of type{" "}
<code class="bg-line/40 px-1 py-0.5">app.bsky.group</code> with
run-unique keys, deleted at the end of each check
<code class="bg-line/40 px-1 py-0.5">
earth.cirrus.check.space
</code>{" "}
with run-unique keys, deleted at the end of each check
</li>
<li>
<span class="text-ink">·</span> Write and delete records in the
neutral{" "}
<code class="bg-line/40 px-1 py-0.5">test.conformance.*</code>{" "}
<code class="bg-line/40 px-1 py-0.5">earth.cirrus.check.*</code>{" "}
collections inside those probe spaces
</li>
<li>
Expand Down
2 changes: 1 addition & 1 deletion apps/check/src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const SCOPE = "atproto transition:generic";
* suite is meaningless there.
*/
const SPACE_SCOPE =
"space:app.bsky.group?collection=*&manage=create&manage=update&manage=delete";
"space:earth.cirrus.check.space?collection=*&manage=create&manage=update&manage=delete";
export const SPACES_SCOPE = `${SCOPE} ${SPACE_SCOPE}`;

const CALLBACK_PATH = "/oauth/callback";
Expand Down
2 changes: 1 addition & 1 deletion apps/check/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function clientMetadata(origin: string): Record<string, unknown> {
// the write tests, the OAuth conformance flow, and the spaces
// conformance run each request a subset of this registration.
scope:
"atproto transition:generic repo:earth.cirrus.check.testrecord include:site.standard.authFull space:app.bsky.group?collection=*&manage=create&manage=update&manage=delete",
"atproto transition:generic repo:earth.cirrus.check.testrecord include:site.standard.authFull space:earth.cirrus.check.space?collection=*&manage=create&manage=update&manage=delete",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
application_type: "web",
Expand Down
57 changes: 57 additions & 0 deletions packages/space-conformance/probe-lexicons/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Probe lexicons

The suite's probe spaces use the space type `earth.cirrus.check.space` and
write records in `earth.cirrus.check.note` / `earth.cirrus.check.withblob`.
These NSIDs are **published, resolvable lexicons** — this directory holds
the canonical documents.

## Why published probes matter

An earlier iteration used `app.bsky.group` (the proposal's example type)
and deliberately-unresolvable `test.conformance.*` collections. That works
against implementations that are lenient about unresolvable lexicons
(Cirrus, the reference alpha) but fails against any implementation that
resolves lexicons dynamically and validates strictly — reported by a
third-party implementer running the web checker. A conformance suite must
not depend on the target's leniency, so the probe NSIDs live under an
authority the suite's maintainers control and resolve for everyone. The
probe collections are still invisible to Bluesky: nothing under
`earth.cirrus.check.*` is an `app.bsky.*` lexicon, so AppViews ignore it.

## How they are published

Lexicon resolution is two steps: DNS names the authority, the authority's
repo holds the schema records.

1. DNS, on the `check.cirrus.earth` zone (covers every
`earth.cirrus.check.*` NSID):

```
_lexicon.check.cirrus.earth. TXT "did=did:plc:uwbl4k3tza7eyjv3morkrld2"
```

2. One `com.atproto.lexicon.schema` record per NSID in that DID's repo,
rkey = the NSID, value = the JSON document in this directory.

To (re)publish after editing a document here, with the maintainer's PDS
token in `$AUTH_TOKEN`, from this directory:

```sh
for f in earth.cirrus.check.space earth.cirrus.check.note earth.cirrus.check.withblob; do
curl -s -X POST https://mk.pds.mk.gg/xrpc/com.atproto.repo.putRecord \
-H "Authorization: Bearer $AUTH_TOKEN" -H 'Content-Type: application/json' \
-d "$(python3 -c "import json;print(json.dumps({
'repo':'did:plc:uwbl4k3tza7eyjv3morkrld2',
'collection':'com.atproto.lexicon.schema',
'rkey':'$f',
'record':json.load(open('$f.json'))}))")"
echo " $f"
done
```

Verify resolution end-to-end:

```sh
dig +short TXT _lexicon.check.cirrus.earth
curl -s "https://mk.pds.mk.gg/xrpc/com.atproto.repo.getRecord?repo=did:plc:uwbl4k3tza7eyjv3morkrld2&collection=com.atproto.lexicon.schema&rkey=earth.cirrus.check.space"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$type": "com.atproto.lexicon.schema",
"lexicon": 1,
"id": "earth.cirrus.check.note",
"defs": {
"main": {
"type": "record",
"description": "A disposable probe record written into a probe space by the space conformance suite, deleted with the space.",
"key": "any",
"record": {
"type": "object",
"required": ["text"],
"properties": {
"text": { "type": "string", "maxLength": 1000 }
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$type": "com.atproto.lexicon.schema",
"lexicon": 1,
"id": "earth.cirrus.check.space",
"defs": {
"main": {
"type": "space",
"name": "Conformance probe space",
"description": "Ephemeral probe spaces created by the @getcirrus/space-conformance suite (check.cirrus.earth). Each is created with a run-unique key and deleted when its check finishes.",
"collections": ["earth.cirrus.check.note", "earth.cirrus.check.withblob"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$type": "com.atproto.lexicon.schema",
"lexicon": 1,
"id": "earth.cirrus.check.withblob",
"defs": {
"main": {
"type": "record",
"description": "A probe record referencing a blob, used by the space conformance suite to verify space blobs are not served publicly. Deleted with its probe space.",
"key": "any",
"record": {
"type": "object",
"required": ["file"],
"properties": {
"file": { "type": "blob", "maxSize": 1048576 }
}
}
}
}
}
4 changes: 2 additions & 2 deletions packages/space-conformance/src/checks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const authGetRecordRequiresAuth = defineCheck({
// all (there's simply no record at this probe URI to leak), and a 5xx
// is a server fault, not a refusal — neither demonstrates enforcement.
const res = await xrpcGet(ctx, "com.atproto.space.getRecord", {
space: `at://${ctx.target.did}/space/app.bsky.group/probe`,
space: `at://${ctx.target.did}/space/earth.cirrus.check.space/probe`,
repo: ctx.target.did,
collection: "app.bsky.feed.post",
rkey: "whatever",
Expand All @@ -96,7 +96,7 @@ const authCredentialRequiresToken = defineCheck({
needs: [],
async run(ctx) {
const res = await xrpcPost(ctx, "com.atproto.space.getSpaceCredential", {
space: `at://${ctx.target.did}/space/app.bsky.group/probe`,
space: `at://${ctx.target.did}/space/earth.cirrus.check.space/probe`,
});
if (res.status === 401 || res.status === 403 || res.status === 400) {
return pass(`refused with ${res.status} ${res.error ?? ""}`);
Expand Down
17 changes: 9 additions & 8 deletions packages/space-conformance/src/checks/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import {
import { createDpopKey, createDpopProofJwt } from "../dpop.js";
import { readCarHeader } from "../car.js";

// A collection with no loaded lexicon, so record validation is
// optimistic (fail-open) on any conformant target — the checks probe
// space behaviour, not Bluesky schema validation.
export const POST = "test.conformance.note";
// The probe collections and space type are published, resolvable
// lexicons under the suite's own authority (see ../../probe-lexicons) —
// so strict, dynamically-resolving targets can validate them, while
// AppViews ignore them (nothing under earth.cirrus.check.* is app.bsky.*).
export const POST = "earth.cirrus.check.note";
export const note = (text: string) => ({ $type: POST, text });

const PROBE_TYPE = "app.bsky.group";
const PROBE_TYPE = "earth.cirrus.check.space";

export interface ProbeSpace {
uri: string;
Expand Down Expand Up @@ -693,7 +694,7 @@ const simplespaceUnsupportedPolicy = defineCheck({
asOperator(ctx),
"com.atproto.simplespace.createSpace",
{
type: "app.bsky.group",
type: PROBE_TYPE,
policy: { $type: "com.example.mysteryPolicy" },
appAccess: { $type: "com.atproto.simplespace.defs#open" },
},
Expand Down Expand Up @@ -805,10 +806,10 @@ const blobsSpaceNotPublic = defineCheck({
const write = await operatorCreateRecord(
ctx,
space.uri,
"test.conformance.withblob",
"earth.cirrus.check.withblob",
"self",
{
$type: "test.conformance.withblob",
$type: "earth.cirrus.check.withblob",
file: blob,
},
);
Expand Down
2 changes: 1 addition & 1 deletion packages/space-conformance/src/cli/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
* space type under the user's own authority. Mirrors the web checker.
*/
const OAUTH_SCOPE =
"atproto transition:generic space:app.bsky.group?collection=*&manage=create&manage=update&manage=delete";
"atproto transition:generic space:earth.cirrus.check.space?collection=*&manage=create&manage=update&manage=delete";

/** How long to wait for the user to complete the browser flow. */
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
Expand Down
Loading