Repository navigation
Description
Motivation
During maintenance, an application and its database may be offline while a reverse proxy or static host remains available. Operators still need to create and sign a notice, serve its public control/key documents, inspect what peers can verify and later publish a cancellation. Requiring a live Federation or importing the application just to obtain a signer defeats that use case.
Provide the tools for static FEP-f799 publication, with shape, authenticity, evidence consistency and currentness reported separately. The same tools can support the online publisher and a standalone CLI without depending on either runtime.
Proposed API
Use the proposed @fedify/fedify/delivery-suspension subpath. The vocabulary child supplies target/fact/parser types. Times below are Temporal.Instant.
function createDeliverySuspensionDraft(input: {
id: string; controller: string; target: DeliverySuspensionTarget;
published: Temporal.Instant; endTime: Temporal.Instant;
startTime?: Temporal.Instant; announcement?: string;
summary?: string; url?: string;
}): DeliverySuspension;
function reviseDeliverySuspension(current: DeliverySuspension | DeliverySuspensionArtifact,
changes: DeliverySuspensionChanges): Promise<DeliverySuspensionTransition>;
function cancelDeliverySuspension(current: DeliverySuspension | DeliverySuspensionArtifact,
options?: { summary?: string }): Promise<DeliverySuspensionTransition>;
interface DeliverySuspensionSigner {
readonly verificationMethod: URL;
readonly publicKey: CryptoKey;
sign(input: Uint8Array, options: { signal?: AbortSignal }): Promise<Uint8Array>;
}
function createDeliverySuspensionSigner(pair: CryptoKeyPair,
verificationMethod: URL): DeliverySuspensionSigner;
function selectDeliverySuspensionSigner(pairs: readonly ActorKeyPair[]):
DeliverySuspensionSigner | undefined;
function signDeliverySuspension(object: DeliverySuspension,
signer: DeliverySuspensionSigner,
options?: { created?: Temporal.Instant; signal?: AbortSignal }):
Promise<DeliverySuspensionArtifact>;
interface DeliverySuspensionArtifact {
readonly contentType: "application/activity+json";
readonly body: string;
readonly bodySha256: string;
readonly facts: DeliverySuspensionFacts;
readonly proofs: readonly ProofSummary[];
}Transitions distinguish a changed unsigned object from no-change/already-cancelled current state. Add validateDeliverySuspensionDraft(), checkDeliverySuspensionTransition() and parseDeliverySuspensionArtifact() for diagnostics. Artifact data and hashes do not carry an authentication brand.
Checks expose checkDeliverySuspensionControl(input, options) for notice-free control preflight, checkDeliverySuspensionSnapshot(subject, {evidence, head?}) and checkDeliverySuspensionLive(id, options). Reports contain passed/failed/incomplete outcome, required/advisory dimension results, per-proof diagnostics, head-relative currentness and explicit not-checked claims. A live call can capture the exact public evidence it used, including requested/final URL, status, content type, body and observation time.
Usage
const controller = "https://social.example/actor";
const verificationMethod = new URL(`${controller}#multikey`);
const target = { kind: "origin", origin: "https://social.example" } as const;
const control = await checkDeliverySuspensionControl(
{ controller, verificationMethod: verificationMethod.href, target },
{ mode: "live", captureEvidence: true },
);
if (control.outcome !== "passed" || !control.capturedEvidence) {
throw new Error("Control preflight failed");
}
const signer = createDeliverySuspensionSigner(keyPair, verificationMethod);
const draft = createDeliverySuspensionDraft({
id: "https://social.example/maintenance/static",
controller, target, published, endTime,
});
const artifact = await signDeliverySuspension(draft, signer);
const report = await checkDeliverySuspensionSnapshot(artifact, {
evidence: control.capturedEvidence,
});
if (report.outcome !== "passed") throw new Error("Snapshot check failed");
// Serve artifact.body at its public ID with artifact.contentType.This proposed example assumes an Ed25519 key pair whose Multikey is already publicly bound to the controller. The operator supplies publication times and actually serves the control metadata, including WebFinger query routing where needed. Snapshot consistency does not prove that those resources are still current. At maintenance completion, cancel the artifact and sign only a changed transition; do not prepublish a future cancellation.
Export and public packaging
Stateless signing may produce a new proof on every call. Idempotent export needs an authoritative owner:
interface DeliverySuspensionExportSource {
readDeliverySuspensionHead(id: string): Promise<DeliverySuspensionHeadSnapshot | null>;
recordDeliverySuspensionArtifact(id: string, expectedHeadVersion: string,
artifact: DeliverySuspensionArtifact): Promise<
| { kind: "recorded" | "already-recorded"; artifact: DeliverySuspensionArtifact }
| { kind: "head-moved" }
>;
}
function exportDeliverySuspension(source: DeliverySuspensionExportSource, id: string,
options?: { signer?: DeliverySuspensionSigner | (() => Promise<DeliverySuspensionSigner>);
signal?: AbortSignal }): Promise<DeliverySuspensionExportResult>;Read head plus artifact atomically. If the slot is already filled, return its recorded bytes without resolving another signer. Otherwise conditionally record and return the same-head first writer's winner in one atomic step. A moved head is a distinct result, not permission to export stale state. headVersion changes for notice state, not fanout progress, receipts or slot filling. Re-signing with a new key requires a separate explicit proof-refresh operation.
For example, an online owner can pass publisher.getExportSource(ctx) to the exporter; a local file workspace can implement the same owner contract. No Federation is required by the interface. Add formatDeliverySuspensionLink(), createDeliverySuspensionEvidenceLoader(), createDeliverySuspensionControlTemplates() and createDeliverySuspensionPublicBundle() for public serving/packaging. A bundle returns public files and warnings; an explicit check remains separate from packaging or deployment.
Security boundaries and alternatives
The signer receives an owned copy of the exact 64-byte eddsa-jcs-2022 signing input. Self-verify its Ed25519 output and capture the secured representation rather than fabricating a second serialization. Select ActorKeyPair.multikey.id, not keyId. Do not add unsupported proof-expiry creation behavior. Public output contains no signer/private key; immutable body strings avoid claiming that readonly arrays or URLs are deeply immutable.
Use the current verifier's all-proofs success behavior with individual expired/unsupported/invalid/key-unavailable diagnostics. Do not infer historical signature validity if expiry caused a proof to be rejected before cryptographic verification. Snapshot checks use supplied evidence and preloaded contexts only, with no hidden network fallback. Live checks use uncached bounded loaders, not CLI KV cache, but do not promise global latestness, all-CDN propagation or peer acceptance.
Visible facts and raw-body hashes cannot establish full same-revision state equality. Preserve unknown nested state, nested proofs and list order; only a verified root proof refresh may be excluded by a conservative internal versioned comparison. Unproven differences are incomplete, not automatically a fork, and unauthenticated higher revisions are not authoritative conflicts. Keep that algorithm out of a frozen public fingerprint API until tested.
Implicitly dropping unknown signed fields, silently adding an announcement to a fetched-only notice, optional unfenced artifact recording and public future-cancellation bundles were rejected. Diagnostics may package a structurally valid artifact with warnings, but packaging success must not claim authenticity or operational readiness.
Shipping criteria and docs
Test signature input/captured output parity, invalid signer output, transitions/no-ops, terminal cancellation, unknown fields, multiple/expired proofs, offline zero-network behavior, actual control evidence, head-recording races and public/secret boundaries. Keep any shared verifier stateless so the receiver can reuse it without requiring this publisher API or a runtime instance.
Document standalone workflows, serving prerequisites and check limitations in docs/manual/delivery-suspension.md, with relevant actor.md, webfinger.md, send.md proof links and public JSDoc. Add actor/FEP skill updates where needed, examples using package imports and package fragments. Record precise static-publication scope in FEDERATION.md. This issue is independently releasable without Atomic KV, an online publisher or CLI.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Activity