diff --git a/.changeset/probe-lexicons.md b/.changeset/probe-lexicons.md
new file mode 100644
index 0000000..37e4411
--- /dev/null
+++ b/.changeset/probe-lexicons.md
@@ -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.
diff --git a/apps/check/src/App.tsx b/apps/check/src/App.tsx
index c5607f0..60f495f 100644
--- a/apps/check/src/App.tsx
+++ b/apps/check/src/App.tsx
@@ -751,13 +751,15 @@ function ConfirmSpacesView(props: {
-
· Create probe spaces of type{" "}
-
app.bsky.group with
- run-unique keys, deleted at the end of each check
+
+ earth.cirrus.check.space
+ {" "}
+ with run-unique keys, deleted at the end of each check
-
· Write and delete records in the
neutral{" "}
-
test.conformance.*{" "}
+ earth.cirrus.check.*{" "}
collections inside those probe spaces
-
diff --git a/apps/check/src/lib/oauth.ts b/apps/check/src/lib/oauth.ts
index 2d5771e..298e466 100644
--- a/apps/check/src/lib/oauth.ts
+++ b/apps/check/src/lib/oauth.ts
@@ -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";
diff --git a/apps/check/src/worker.ts b/apps/check/src/worker.ts
index c71a7eb..91c33f5 100644
--- a/apps/check/src/worker.ts
+++ b/apps/check/src/worker.ts
@@ -29,7 +29,7 @@ function clientMetadata(origin: string): Record {
// 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",
diff --git a/packages/space-conformance/probe-lexicons/README.md b/packages/space-conformance/probe-lexicons/README.md
new file mode 100644
index 0000000..dd95473
--- /dev/null
+++ b/packages/space-conformance/probe-lexicons/README.md
@@ -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"
+```
diff --git a/packages/space-conformance/probe-lexicons/earth.cirrus.check.note.json b/packages/space-conformance/probe-lexicons/earth.cirrus.check.note.json
new file mode 100644
index 0000000..4ec10b6
--- /dev/null
+++ b/packages/space-conformance/probe-lexicons/earth.cirrus.check.note.json
@@ -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 }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/space-conformance/probe-lexicons/earth.cirrus.check.space.json b/packages/space-conformance/probe-lexicons/earth.cirrus.check.space.json
new file mode 100644
index 0000000..cc92625
--- /dev/null
+++ b/packages/space-conformance/probe-lexicons/earth.cirrus.check.space.json
@@ -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"]
+ }
+ }
+}
diff --git a/packages/space-conformance/probe-lexicons/earth.cirrus.check.withblob.json b/packages/space-conformance/probe-lexicons/earth.cirrus.check.withblob.json
new file mode 100644
index 0000000..68b639f
--- /dev/null
+++ b/packages/space-conformance/probe-lexicons/earth.cirrus.check.withblob.json
@@ -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 }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/space-conformance/src/checks/base.ts b/packages/space-conformance/src/checks/base.ts
index 8b4481b..96a816a 100644
--- a/packages/space-conformance/src/checks/base.ts
+++ b/packages/space-conformance/src/checks/base.ts
@@ -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",
@@ -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 ?? ""}`);
diff --git a/packages/space-conformance/src/checks/operator.ts b/packages/space-conformance/src/checks/operator.ts
index 372b648..f3fd836 100644
--- a/packages/space-conformance/src/checks/operator.ts
+++ b/packages/space-conformance/src/checks/operator.ts
@@ -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;
@@ -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" },
},
@@ -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,
},
);
diff --git a/packages/space-conformance/src/cli/oauth.ts b/packages/space-conformance/src/cli/oauth.ts
index 3947c87..f180629 100644
--- a/packages/space-conformance/src/cli/oauth.ts
+++ b/packages/space-conformance/src/cli/oauth.ts
@@ -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;