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
10 changes: 10 additions & 0 deletions src/app/.well-known/openswarm-hub.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @route GET /.well-known/openswarm-hub.json — the hub record, where clients look first. */
import { NextRequest } from 'next/server';
import { hubRecord } from '@/lib/openswarm/service';
import { json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
return json(hubRecord(new URL(request.url).origin));
}
16 changes: 16 additions & 0 deletions src/app/api/openswarm/hub/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @route GET /api/openswarm/hub — the hub record.
*
* Also served at /.well-known/openswarm-hub.json, which is where the spec says
* a client looks. It says who this hub is, what it charges, which consent
* bases it will list, and that it carries all four lanes.
*/
import { NextRequest } from 'next/server';
import { hubRecord } from '@/lib/openswarm/service';
import { json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
return json(hubRecord(new URL(request.url).origin));
}
17 changes: 17 additions & 0 deletions src/app/api/openswarm/pay2seed/attestations/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @route GET /api/openswarm/pay2seed/attestations/[id] — the record and where it stands. */
import { NextRequest } from 'next/server';
import { getAttestation } from '@/lib/openswarm/service';
import { failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
const attestation = await getAttestation(decodeURIComponent(id));
if (!attestation) return json({ error: 'no such attestation' }, 404);
return json({ attestation });
} catch (error) {
return failed(error);
}
}
23 changes: 23 additions & 0 deletions src/app/api/openswarm/pay2seed/attestations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @route POST /api/openswarm/pay2seed/attestations — register consent.
*
* Nothing is listed on this hub without one. The record is signed by the key
* that claims it, carries a README, and says on what basis it may be shared.
* A public claim is visible for a window first, so a notice can void it before
* anyone is paid to seed something the requester had no right to.
*/
import { NextRequest } from 'next/server';
import { registerAttestation } from '@/lib/openswarm/service';
import { body, failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function POST(request: NextRequest) {
try {
const record = await body<unknown>(request);
const { attestation, id } = await registerAttestation(record);
return json({ id, attestation }, 201);
} catch (error) {
return failed(error);
}
}
17 changes: 17 additions & 0 deletions src/app/api/openswarm/pay2seed/leases/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @route GET /api/openswarm/pay2seed/leases/[id] — a lease, what it has earned, where it stands. */
import { NextRequest } from 'next/server';
import { getLease } from '@/lib/openswarm/service';
import { failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
const lease = await getLease(decodeURIComponent(id));
if (!lease) return json({ error: 'no such lease' }, 404);
return json({ lease });
} catch (error) {
return failed(error);
}
}
26 changes: 26 additions & 0 deletions src/app/api/openswarm/pay2seed/leases/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @route POST /api/openswarm/pay2seed/leases — take a slot on an offer.
*
* The seeder proves it holds the key by signing the offer id. It must already
* be a payee, and clear the hub's standing floor. The lane is stamped here, at
* the moment the money is agreed, so it survives either party later changing
* kind.
*/
import { NextRequest } from 'next/server';
import { takeLease } from '@/lib/openswarm/service';
import { body, failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function POST(request: NextRequest) {
try {
const input = await body<{ offer?: string; seeder?: string; sig?: string }>(request);
if (!input.offer || !input.seeder || !input.sig) {
return json({ error: 'offer, seeder and sig are required' }, 400);
}
const lease = await takeLease({ offerId: input.offer, seederKey: input.seeder, sig: input.sig });
return json({ lease }, 201);
} catch (error) {
return failed(error);
}
}
47 changes: 47 additions & 0 deletions src/app/api/openswarm/pay2seed/notices/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @route POST /api/openswarm/pay2seed/notices — a claim against an attestation.
*
* Anyone may file one. The hub records it and forwards it to the requester's
* notice endpoint; whether the attestation is voided or stands is reviewed,
* and a hub that voided on nothing at all would not be conformant either way.
*/
import { NextRequest } from 'next/server';
import { fileNotice } from '@/lib/openswarm/service';
import { body, failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function POST(request: NextRequest) {
try {
const input = await body<{
attestation?: string;
kind?: 'rights' | 'illegal' | 'personal-data' | 'other';
statement?: string;
claimant?: { name?: string; contact?: string };
record?: unknown;
}>(request);
if (!input.attestation || !input.statement) {
return json({ error: 'attestation and statement are required' }, 400);
}
const result = await fileNotice({
attestationId: input.attestation,
kind: input.kind ?? 'other',
statement: input.statement,
claimantName: input.claimant?.name ?? null,
claimantContact: input.claimant?.contact ?? null,
record: input.record,
});
return json(
{
notice: result.noticeId,
attestation: result.attestation.id,
// Say plainly what happens next rather than implying it is already done.
outcome: 'received',
noticeEndpoint: result.attestation.noticeEndpoint,
},
201
);
} catch (error) {
return failed(error);
}
}
32 changes: 32 additions & 0 deletions src/app/api/openswarm/pay2seed/offers/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @route GET /api/openswarm/pay2seed/offers/[id] — an offer, its attestation and its leases. */
import { NextRequest } from 'next/server';
import { getAttestation, getOffer } from '@/lib/openswarm/service';
import { failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
const offer = await getOffer(decodeURIComponent(id));
if (!offer) return json({ error: 'no such offer' }, 404);
const attestation = await getAttestation(offer.attestationId);
return json({
offer,
attestation: attestation
? {
id: attestation.id,
basis: attestation.basis,
visibility: attestation.visibility,
license: attestation.license,
description: attestation.description,
subject: attestation.subject,
status: attestation.status,
readme: attestation.readme,
}
: null,
});
} catch (error) {
return failed(error);
}
}
48 changes: 48 additions & 0 deletions src/app/api/openswarm/pay2seed/offers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @route GET /api/openswarm/pay2seed/offers — the market a seeder picks work from.
* @route POST /api/openswarm/pay2seed/offers — post an offer against an attestation.
*
* The market only shows offers whose attestation is honoured, which is what
* the claim window is for. Every row carries the basis and the visibility, so a
* seeder knows before accepting whether it would be holding a stranger's
* ciphertext or seeding an openly licensed dataset in the clear.
*/
import { NextRequest } from 'next/server';
import { createOffer, listMarket, quoteOffer, type OfferDraft } from '@/lib/openswarm/service';
import { body, failed, json } from '@/lib/openswarm/route-helpers';
import type { Basis, Visibility } from '@/lib/openswarm/types';

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
try {
const url = new URL(request.url);
const rows = await listMarket({
visibility: (url.searchParams.get('visibility') as Visibility) ?? undefined,
basis: (url.searchParams.get('basis') as Basis) ?? undefined,
limit: Number(url.searchParams.get('limit') ?? 50),
});
return json({ offers: rows });
} catch (error) {
return failed(error);
}
}

export async function POST(request: NextRequest) {
try {
const input = await body<OfferDraft & { requester?: string; quoteOnly?: boolean }>(request);
if (!input.requester) return json({ error: 'requester is required' }, 400);
if (!input.attestation) return json({ error: 'attestation is required' }, 400);

// A quote is free and pure, so a page can show the price without
// committing anyone to it.
if (input.quoteOnly) return json({ quote: quoteOffer(input) });

const { offer, quote } = await createOffer(input, input.requester);
// Unpaid until the money settles: a budget that is not escrowed is not a
// promise anybody should seed against.
return json({ offer, quote, status: 'unpaid' }, 201);
} catch (error) {
return failed(error);
}
}
17 changes: 17 additions & 0 deletions src/app/api/openswarm/pay2seed/parties/[key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @route GET /api/openswarm/pay2seed/parties/[key] — a key's standing and balance. */
import { NextRequest } from 'next/server';
import { getParty } from '@/lib/openswarm/service';
import { failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(_request: NextRequest, { params }: { params: Promise<{ key: string }> }) {
try {
const { key } = await params;
const party = await getParty(decodeURIComponent(key));
if (!party) return json({ error: 'no such key here' }, 404);
return json({ party });
} catch (error) {
return failed(error);
}
}
32 changes: 32 additions & 0 deletions src/app/api/openswarm/pay2seed/parties/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @route POST /api/openswarm/pay2seed/parties — register a key.
*
* A party says what it is: a human, or a bit (an autonomous agent). An agent
* that wants to sell in public names the operator answerable for it. A payout
* address is what makes a key a payee, and without one it cannot take a lease,
* because there would be nowhere to pay.
*/
import { NextRequest } from 'next/server';
import { registerParty } from '@/lib/openswarm/service';
import { body, failed, json } from '@/lib/openswarm/route-helpers';
import type { PartyKind } from '@/lib/openswarm/lanes';

export const dynamic = 'force-dynamic';

export async function POST(request: NextRequest) {
try {
const input = await body<{
key?: string;
kind?: PartyKind;
operatorKey?: string | null;
label?: string | null;
payoutAddress?: string | null;
payoutNetwork?: string | null;
}>(request);
if (!input.key) return json({ error: 'key is required' }, 400);
const party = await registerParty({ ...input, key: input.key });
return json({ party }, 201);
} catch (error) {
return failed(error);
}
}
56 changes: 56 additions & 0 deletions src/app/api/openswarm/pay2seed/proofs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @route POST /api/openswarm/pay2seed/proofs — report a period's verdict.
*
* A passed period is paid at once; the receipt's unique (lease, period) is the
* whole of the idempotency, so a verifier that reports twice pays once. Two
* consecutive failures end the lease and reopen the slot.
*
* Only the hub's own verifier may report today. A seeder cannot mark its own
* period proven, which is the point of a proof.
*/
import { NextRequest } from 'next/server';
import { reportProof } from '@/lib/openswarm/service';
import { body, failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

function authorised(request: NextRequest): boolean {
const expected = process.env.OPENSWARM_VERIFIER_TOKEN;
if (!expected) return false;
const header = request.headers.get('authorization') ?? '';
return header === `Bearer ${expected}`;
}

export async function POST(request: NextRequest) {
try {
if (!authorised(request)) return json({ error: 'a verifier token is required to report a proof' }, 401);
const input = await body<{
lease?: string;
period?: number;
kind?: 'challenge' | 'probe';
passed?: boolean;
verifier?: string | null;
detail?: string | null;
record?: unknown;
}>(request);
if (!input.lease || typeof input.period !== 'number' || typeof input.passed !== 'boolean') {
return json({ error: 'lease, period and passed are required' }, 400);
}
const result = await reportProof({
leaseId: input.lease,
period: input.period,
kind: input.kind ?? 'probe',
passed: input.passed,
verifierKey: input.verifier ?? null,
detail: input.detail ?? null,
record: input.record,
});
return json({
lease: result.lease,
earnedUsd: result.earnedUsd,
alreadyRecorded: result.alreadyRecorded,
});
} catch (error) {
return failed(error);
}
}
28 changes: 28 additions & 0 deletions src/app/api/openswarm/pay2seed/seeders/[key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @route GET /api/openswarm/pay2seed/seeders/[key] — a seeder's standing, balance and leases. */
import { NextRequest } from 'next/server';
import { getParty, listLeasesFor } from '@/lib/openswarm/service';
import { failed, json } from '@/lib/openswarm/route-helpers';

export const dynamic = 'force-dynamic';

export async function GET(_request: NextRequest, { params }: { params: Promise<{ key: string }> }) {
try {
const { key } = await params;
const seeder = decodeURIComponent(key);
const party = await getParty(seeder);
if (!party) return json({ error: 'no such key here' }, 404);
const leases = await listLeasesFor(seeder);
return json({
key: party.key,
kind: party.kind,
standing: party.seederStanding,
proven: party.proven,
failed: party.failed,
abandoned: party.abandoned,
balanceUsd: party.balanceUsd,
leases,
});
} catch (error) {
return failed(error);
}
}
Loading
Loading