Skip to content

Commit e66cb39

Browse files
committed
feat: harden Registry artifact workflows
1 parent fc8364d commit e66cb39

12 files changed

Lines changed: 1244 additions & 95 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
## Unreleased
44

5+
- Harden the unified artifact Registry boundary: default discovery now hides
6+
pending/rejected releases and paginates by package coordinate; deployment
7+
records and admin recovery must match the immutable CKB `hash_type` and
8+
`dep_type`; generated CellDep descriptors re-query mainnet and reject spent
9+
code/DepGroup Cells; RPC calls are time- and size-bounded; and deployment
10+
capability use commits with the chain-verified state. Positive static-mirror
11+
publication now follows database admission, while suppressive states are
12+
mirrored first to fail closed; deferred sync is audited rather than
13+
advertising uncommitted positive state. Add the capability-signed
14+
`cellc artifact set-availability` publisher path used by Manage, defensive
15+
frontend page deduplication, and complete `Artifact.toml` plus bundle
16+
scaffolding for non-CellScript submissions.
517
- Redesign the Registry submission and package-maintenance surfaces around
618
contextual, task-first workflows: remove the public `Manage` tab and
719
redundant form controls, link maintenance from package details, guide first

docs/CELLSCRIPT_GATE_POLICY.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ service coverage, not evidence
6666
that Cloudflare, R2, Hyperdrive, Neon, DNS, or a production deployment works.
6767
The CLI coverage includes the explicit first-publish admission sequence:
6868
`cellc auth capability submit`, `cellc auth namespace claim`, then
69-
`cellc publish`. Capability registration does not silently claim a namespace;
69+
`cellc publish`; publisher maintenance additionally uses the capability-signed
70+
`cellc artifact set-availability` path, and `cellc artifact cell-dep` performs a
71+
fresh mainnet liveness check before producing a transaction-builder descriptor.
72+
Capability registration does not silently claim a namespace;
7073
the claim response must be `active` before the write API accepts a version.
7174
Registry API tests pin both accepted publisher roots: JoyID signatures under
7275
`principal_type = joyid_ckb` and recoverable CKB message signatures under

docs/CELLSCRIPT_REGISTRY_PHASE1.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ cellc artifact verify --bundle vault-lock.bundle.json --receipt vault-lock.bundl
220220
cellc artifact pin acme/vault-lock@1.0.0 --output Artifacts.lock --accept-hash-bound
221221
cellc artifact copy acme/starter@1.0.0 --destination ./new-project --accept-hash-bound
222222
cellc artifact record-deployment acme/vault-lock@1.0.0 --code-hash <hash> --hash-type data1 --dep-type code --tx-hash <tx_hash> --index 0 --capability-key-id <key_id>
223-
cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound
223+
cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound --rpc-url https://mainnet.ckb.dev/rpc
224+
cellc artifact set-availability acme/vault-lock@1.0.0 --status yanked --reason "security advisory" --capability-key-id <key_id>
224225
cellc artifact commitment acme/vault-lock@1.0.0 --output RegistryCommitment.json
225226
```
226227

@@ -230,12 +231,20 @@ exact Registry identity and requires an explicit trust decision for
230231
integrity-only evidence. `copy` is no-overwrite and rejects traversal,
231232
platform-specific, duplicate, or unauthenticated paths. `cell-dep` requires an
232233
attached RPC-verified mainnet deployment and preserves the DepGroup container
233-
and resolved code-member identities. It never turns an `undeployed` release
234-
into a CellDep.
234+
and resolved code-member identities. Before writing `CellDep.json`, it queries
235+
mainnet again, rejects a spent deployment or resolved code member, checks the
236+
RPC chain identity, and rebinds `hash_type` / `dep_type` to the signed profile
237+
contract. It never turns an `undeployed` release into a CellDep.
235238

236239
`record-deployment` derives the artifact/data identity from the signed Registry
237240
release, signs a mainnet-only payload with the scoped capability key, and sends
238-
it to the API for live-Cell verification.
241+
it to the API for live-Cell verification. Both publisher and recovery paths
242+
reject deployment modes that differ from `profile_contract.ckb`.
243+
244+
`set-availability` is the publisher control-plane path used by the Manage UI.
245+
It signs a short-lived, nonce-protected capability payload; publishers may set
246+
`active`, `deprecated`, or `yanked`, while administrative quarantine remains a
247+
separate privileged action.
239248

240249
`commitment` produces the canonical `cellscript-registry-commitment-v1`
241250
payload, CKB Blake2b commitment, and compact `CSREGv1 || hash` Cell data. The
@@ -245,9 +254,10 @@ hash used for chain indexing.
245254

246255
## Publisher Authorisation
247256

248-
The website presents a single “Connect CKB wallet” entry. Its modal lists all
249-
supported CKB wallet connectors, but only connectors actually detected in the
250-
current browser can sign immediately; install links are shown for the rest.
257+
The website presents a single “Connect CKB wallet” entry. Its modal separates
258+
CCC-detected browser signers, which can connect immediately, from wallet
259+
directory entries, which open an official site and continue through the manual
260+
payload/signature path. A directory entry is never reported as connected.
251261
Network selection is not exposed because authorisation and deployment are
252262
mainnet-only.
253263

@@ -272,10 +282,14 @@ GET /v1/artifacts/:namespace/:name/releases/:release/commitment
272282
GET /artifacts/:namespace/:name/releases/:release.json
273283
POST /v1/artifacts/:namespace/:name/releases
274284
POST /v1/artifacts/:namespace/:name/releases/:release/deployments
285+
POST /v1/artifacts/:namespace/:name/releases/:release/availability
275286
```
276287

277288
The list endpoint accepts `q`, `namespace`, `kind`, `verification`,
278-
`deployment`, `availability`, `limit`, and `offset`. Static release objects and
289+
`deployment`, `availability`, `limit`, and `offset`. Without an explicit
290+
`verification` filter, public discovery includes only accepted verification
291+
states and excludes `pending` / `rejected`. Pagination offsets count package
292+
coordinates, not version rows. Static release objects and
279293
immutable bundles are served separately from the write database so consumers
280294
can hash-verify and cache them independently.
281295

@@ -300,7 +314,10 @@ that every artifact is installable.
300314
- Deployment evidence must match the published executable hash and a live
301315
mainnet Cell.
302316
- Quarantined releases are not returned by public detail or evidence routes.
303-
- Immutable bundle writes complete before release admission.
317+
- The database admits positive identity/state atomically before publishing its
318+
mutable static mirror. Suppressive states are mirrored first to fail closed;
319+
other mirror failures are audited and retried by verification sync, so an
320+
uncommitted release or deployment is never advertised as current.
304321
- State transitions append evidence; they do not mutate hash identity.
305322

306323
## Validation

docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,15 @@ cellc artifact fetch acme/vault-lock@1.0.0 --output vault-lock.bundle.json
180180
cellc artifact verify --bundle vault-lock.bundle.json --receipt vault-lock.bundle.json.receipt.json
181181
cellc artifact pin acme/vault-lock@1.0.0 --output Artifacts.lock --accept-hash-bound
182182
cellc artifact record-deployment acme/vault-lock@1.0.0 --code-hash <hash> --hash-type data1 --dep-type code --tx-hash <tx_hash> --index 0 --capability-key-id <key_id>
183-
cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound
183+
cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound --rpc-url https://mainnet.ckb.dev/rpc
184+
cellc artifact set-availability acme/vault-lock@1.0.0 --status yanked --reason "security advisory" --capability-key-id <key_id>
184185
cellc artifact commitment acme/vault-lock@1.0.0 --output RegistryCommitment.json
185186
```
186187

187-
The last two commands fail until mainnet deployment evidence has been verified.
188-
The commitment file contains canonical `CSREGv1` Cell data; attestation still
188+
`cell-dep` fails until mainnet deployment evidence has been verified, then
189+
rechecks that the deployment (and resolved DepGroup code member) is still live
190+
at consumption time. Deployment mode must equal the immutable profile
191+
contract. The commitment file contains canonical `CSREGv1` Cell data; attestation still
189192
requires the API to read a live mainnet Cell and match its Type/Lock identities.
190193

191194
## 6. Other artifact kinds

services/registry-api/src/domain.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const PUBLISH_PROTOCOL = "cellscript-registry-publish-v1";
99
export const PUBLISH_ACTION = "publish";
1010
export const DEPLOYMENT_PROTOCOL = "cellscript-registry-deployment";
1111
export const DEPLOYMENT_ACTION = "record_deployment";
12+
export const AVAILABILITY_PROTOCOL = "cellscript-registry-availability-v1";
13+
export const AVAILABILITY_ACTION = "set_availability";
1214
export const REGISTRY_SCHEMA_VERSION = 1;
1315
export const ARTIFACT_PROFILE_CONTRACT_SCHEMA = "cellscript-registry-profile-contract-v1";
1416
export const CELLSCRIPT_EDITION = "2026";
@@ -124,6 +126,22 @@ export interface DeploymentPayload {
124126
cli_version: string;
125127
}
126128

129+
export interface AvailabilityPayload {
130+
protocol: typeof AVAILABILITY_PROTOCOL;
131+
action: typeof AVAILABILITY_ACTION;
132+
registry_origin: string;
133+
namespace: string;
134+
name: string;
135+
release: string;
136+
availability_status: Exclude<AvailabilityStatus, "quarantined">;
137+
reason?: string;
138+
capability_key_id: string;
139+
nonce: string;
140+
issued_at: string;
141+
expires_at: string;
142+
cli_version: string;
143+
}
144+
127145
export interface RegistryVersionEntry {
128146
version: string;
129147
tag: string;
@@ -419,6 +437,60 @@ export function validateDeploymentPayload(
419437
};
420438
}
421439

440+
export function validateAvailabilityPayload(
441+
input: unknown,
442+
registryOrigin: string,
443+
now: Date,
444+
): AvailabilityPayload {
445+
const value = assertPlainObject(input, "invalid_availability_payload");
446+
if (requireString(value, "protocol") !== AVAILABILITY_PROTOCOL || requireString(value, "action") !== AVAILABILITY_ACTION) {
447+
throw new ApiError(400, "invalid_availability_action", "availability payload has the wrong protocol or action");
448+
}
449+
if (requireString(value, "registry_origin") !== registryOrigin) {
450+
throw new ApiError(400, "invalid_registry_origin", "availability payload registry_origin does not match this API");
451+
}
452+
const availabilityStatus = requireString(value, "availability_status");
453+
if (!(availabilityStatus === "active" || availabilityStatus === "deprecated" || availabilityStatus === "yanked")) {
454+
throw new ApiError(400, "invalid_publisher_availability_status", "publishers may set availability_status to active, deprecated, or yanked");
455+
}
456+
const reason = value["reason"] === undefined ? undefined : requireString(value, "reason").trim();
457+
if (availabilityStatus === "yanked" && !reason) {
458+
throw new ApiError(400, "availability_reason_required", "yanking a release requires a reason");
459+
}
460+
if (reason && reason.length > 500) {
461+
throw new ApiError(400, "invalid_availability_reason", "availability reason must be no longer than 500 characters");
462+
}
463+
const capabilityKeyId = requireString(value, "capability_key_id");
464+
if (!/^cap_[0-9a-f]{32}$/.test(capabilityKeyId)) {
465+
throw new ApiError(400, "invalid_capability_key_id", "capability_key_id is malformed");
466+
}
467+
const nonce = requireString(value, "nonce");
468+
if (!/^0x[0-9a-fA-F]{16,}$/.test(nonce)) {
469+
throw new ApiError(400, "invalid_nonce", "nonce must be hex and at least 8 bytes");
470+
}
471+
const issuedAt = requireString(value, "issued_at");
472+
const expiresAt = requireString(value, "expires_at");
473+
parseTimestamp(issuedAt, "issued_at");
474+
if (parseTimestamp(expiresAt, "expires_at").getTime() <= now.getTime()) {
475+
throw new ApiError(401, "availability_payload_expired", "availability payload has expired");
476+
}
477+
return {
478+
protocol: AVAILABILITY_PROTOCOL,
479+
action: AVAILABILITY_ACTION,
480+
registry_origin: registryOrigin,
481+
namespace: validatePackageIdent(requireString(value, "namespace"), "namespace"),
482+
name: validatePackageIdent(requireString(value, "name"), "name"),
483+
release: validateVersion(requireString(value, "release")),
484+
availability_status: availabilityStatus,
485+
...(reason ? { reason } : {}),
486+
capability_key_id: capabilityKeyId,
487+
nonce,
488+
issued_at: issuedAt,
489+
expires_at: expiresAt,
490+
cli_version: requireString(value, "cli_version"),
491+
};
492+
}
493+
422494
export function sameCkbHash(left: string, right: string): boolean {
423495
return left.replace(/^0x/, "").toLowerCase() === right.replace(/^0x/, "").toLowerCase();
424496
}

0 commit comments

Comments
 (0)