Skip to content

Commit c29c75e

Browse files
committed
Add ACTA Pilot styles and documentation
- Introduced a new CSS file for the ACTA Pilot page with styles for layout, components, and responsive design. - Created a comprehensive markdown document detailing the ACTA x Sub Rosa Pilot, including purpose, flow, trust model, configuration, and integration guidelines.
1 parent eca7c39 commit c29c75e

16 files changed

Lines changed: 2719 additions & 2 deletions

File tree

apps/web/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ VITE_TRUSTLESS_WORK_TRUSTLINE_CONTRACT_ID=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4W
2626
VITE_TRUSTLESS_WORK_TRUSTLINE_SYMBOL=USDC
2727
VITE_TRUSTLESS_WORK_TRUSTLINE_ADDRESS=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
2828

29+
# --- ACTA x Sub Rosa credential pilot ---
30+
# Optional override. ACTA Testnet is the default.
31+
VITE_ACTA_BASE_URL=https://api.testnet.acta.build
32+
# Local pilot convenience only. VITE_* values are embedded in the client build,
33+
# so do not use a sensitive or production key here.
34+
VITE_ACTA_API_KEY=
35+
2936
# --- Legacy v1 interactive app ---
3037
# VITE_CONTRACT_ID=CC2QMOXZERI6UOR67YKSORT7QTUHQ5QUGMHQBYVP23YM3NMUNNOEOGZY
3138

apps/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
"dev": "vite",
99
"build": "vite build",
1010
"preview": "vite preview",
11-
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/config/routing.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/lib/pilotConcurrency.test.ts src/lib/pilotReveal.test.ts src/lib/pilotSubmission.test.ts src/lib/offerHubPilot.test.ts src/integrations/trustless-work/index.test.ts src/components/dashboard/RoundStatusCard.test.tsx",
11+
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/config/routing.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/lib/pilotConcurrency.test.ts src/lib/pilotReveal.test.ts src/lib/pilotSubmission.test.ts src/lib/offerHubPilot.test.ts src/lib/actaPilot.test.ts src/integrations/acta/index.test.ts src/integrations/trustless-work/index.test.ts src/components/dashboard/RoundStatusCard.test.tsx",
1212
"typecheck": "tsc --noEmit -p tsconfig.json"
1313
},
1414
"dependencies": {
15+
"@acta-team/credentials": "^1.1.8",
1516
"@stellar/freighter-api": "^6.0.1",
1617
"@stellar/stellar-sdk": "^15.1.0",
1718
"@sub-rosa/agent": "workspace:*",

apps/web/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PilotPage } from "./pages/PilotPage";
1313
import { SignalPilotPage } from "./pages/SignalPilotPage";
1414
import { TrustlessWorkPilotPage } from "./pages/TrustlessWorkPilotPage";
1515
import { OfferHubPilotPage } from "./pages/OfferHubPilotPage";
16+
import { ActaPilotPage } from "./pages/ActaPilotPage";
1617
import { ToastProvider } from "./ui/Toast";
1718

1819
export default function App() {
@@ -50,6 +51,8 @@ export default function App() {
5051
<TrustlessWorkPilotPage goHome={() => navigate("landing")} />
5152
) : route.page === "offerHubPilot" ? (
5253
<OfferHubPilotPage goHome={() => navigate("landing")} />
54+
) : route.page === "actaPilot" ? (
55+
<ActaPilotPage goHome={() => navigate("landing")} />
5356
) : route.page === "docs" ? (
5457
<DocsPage goHome={() => navigate("landing")} />
5558
) : (

apps/web/src/config/routing.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
22
import { test } from "node:test";
33

44
import {
5+
actaPilotRoundIdFromHash,
56
hashFor,
67
offerHubPilotRoundIdFromHash,
78
pilotRoundIdFromHash,
@@ -34,6 +35,10 @@ test("pilot routes open the partner workspace", () => {
3435
page: "offerHubPilot",
3536
useCase: "auction",
3637
});
38+
assert.deepEqual(routeFromHash("#/pilot/acta"), {
39+
page: "actaPilot",
40+
useCase: "auction",
41+
});
3742
});
3843

3944
test("pilot round links accept only numeric round ids", () => {
@@ -49,6 +54,13 @@ test("pilot navigation emits the canonical workspace hash", () => {
4954
assert.equal(hashFor("signalPilot"), "#/pilot/the-signal");
5055
assert.equal(hashFor("trustlessWorkPilot"), "#/pilot/trustless-work");
5156
assert.equal(hashFor("offerHubPilot"), "#/pilot/offer-hub");
57+
assert.equal(hashFor("actaPilot"), "#/pilot/acta");
58+
});
59+
60+
test("ACTA pilot round links accept only numeric round ids", () => {
61+
assert.equal(actaPilotRoundIdFromHash("#/pilot/acta/42"), "42");
62+
assert.equal(actaPilotRoundIdFromHash("#pilot/acta/0007"), "0007");
63+
assert.equal(actaPilotRoundIdFromHash("#/pilot/acta/not-a-round"), "");
5264
});
5365

5466
test("offer-hub pilot round links accept only numeric round ids", () => {

apps/web/src/config/routing.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type Page =
1111
| "signalPilot"
1212
| "trustlessWorkPilot"
1313
| "offerHubPilot"
14+
| "actaPilot"
1415
| "docs";
1516

1617
export interface RouteState {
@@ -41,6 +42,9 @@ export function routeFromHash(source = window.location.hash): RouteState {
4142
if (parts[1] === "offer-hub") {
4243
return { page: "offerHubPilot", useCase: "auction" };
4344
}
45+
if (parts[1] === "acta") {
46+
return { page: "actaPilot", useCase: "auction" };
47+
}
4448
if (parts[1] === "basic" || /^\d+$/.test(parts[1] ?? "")) {
4549
return { page: "basicPilot", useCase: "auction" };
4650
}
@@ -74,6 +78,7 @@ export function hashFor(page: Page, useCase: UseCaseId = "auction"): string {
7478
if (page === "signalPilot") return "#/pilot/the-signal";
7579
if (page === "trustlessWorkPilot") return "#/pilot/trustless-work";
7680
if (page === "offerHubPilot") return "#/pilot/offer-hub";
81+
if (page === "actaPilot") return "#/pilot/acta";
7782
if (page === "docs") return "#/docs";
7883
return `#/demo/${useCase}`;
7984
}
@@ -91,3 +96,10 @@ export function offerHubPilotRoundIdFromHash(source = window.location.hash): str
9196
? parts[2]
9297
: "";
9398
}
99+
100+
export function actaPilotRoundIdFromHash(source = window.location.hash): string {
101+
const parts = source.replace(/^#\/?/, "").split("/").filter(Boolean);
102+
return parts[0] === "pilot" && parts[1] === "acta" && /^\d+$/.test(parts[2] ?? "")
103+
? parts[2]
104+
: "";
105+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import {
5+
ActaAdapter,
6+
actaOutcomeCredentialId,
7+
assertOutcomeEvidence,
8+
buildActaOutcomeCredential,
9+
evaluateActaEligibility,
10+
isValidActaIssuerDid,
11+
type ActaOutcomeEvidence,
12+
} from "./index.js";
13+
14+
const ISSUER = `did:stellar:testnet:G${"A".repeat(55)}`;
15+
const OTHER_ISSUER = `did:stellar:testnet:G${"B".repeat(55)}`;
16+
const SUBJECT = `did:stellar:testnet:G${"C".repeat(55)}`;
17+
18+
function credential(issuer = ISSUER) {
19+
return {
20+
result: {
21+
"@context": ["https://www.w3.org/ns/credentials/v2"],
22+
type: ["VerifiableCredential", "VerifiedProviderCredential"],
23+
issuer,
24+
credentialSubject: { id: SUBJECT, privateClaim: "must not be retained" },
25+
},
26+
};
27+
}
28+
29+
function evidence(overrides: Partial<ActaOutcomeEvidence> = {}): ActaOutcomeEvidence {
30+
return {
31+
roundId: "42",
32+
network: "testnet",
33+
subjectWallet: `G${"D".repeat(55)}`,
34+
subjectDid: SUBJECT,
35+
validReveal: true,
36+
selectedProviderWallet: `G${"D".repeat(55)}`,
37+
...overrides,
38+
};
39+
}
40+
41+
test("maps ACTA status, type, subject, and trusted issuer into eligibility", () => {
42+
const result = evaluateActaEligibility({
43+
policy: { credentialType: "VerifiedProviderCredential", trustedIssuerDid: ISSUER },
44+
owner: `G${"D".repeat(55)}`,
45+
credentialId: "provider-credential",
46+
status: "valid",
47+
credential: credential(),
48+
checkedAt: "2026-08-11T00:00:00.000Z",
49+
});
50+
assert.equal(result.state, "eligible");
51+
assert.equal(result.issuerDid, ISSUER);
52+
assert.equal(result.subjectDid, SUBJECT);
53+
assert.equal(JSON.stringify(result).includes("privateClaim"), false);
54+
});
55+
56+
test("rejects an otherwise valid credential from an untrusted issuer", () => {
57+
const result = evaluateActaEligibility({
58+
policy: { credentialType: "VerifiedProviderCredential", trustedIssuerDid: ISSUER },
59+
owner: `G${"D".repeat(55)}`,
60+
credentialId: "provider-credential",
61+
status: "valid",
62+
credential: credential(OTHER_ISSUER),
63+
});
64+
assert.equal(result.state, "not_eligible");
65+
assert.match(result.message, /not trusted/);
66+
});
67+
68+
test("validates did:stellar issuer syntax and network", () => {
69+
assert.equal(isValidActaIssuerDid(ISSUER, "testnet"), true);
70+
assert.equal(isValidActaIssuerDid(ISSUER, "mainnet"), false);
71+
assert.equal(isValidActaIssuerDid("GNOT_A_DID", "testnet"), false);
72+
});
73+
74+
test("outcome credentials require a real revealed source event", () => {
75+
assert.throws(
76+
() => assertOutcomeEvidence("round_participation", evidence({ validReveal: false })),
77+
/valid revealed/,
78+
);
79+
assert.throws(
80+
() => assertOutcomeEvidence("selected_provider", evidence({ selectedProviderWallet: null })),
81+
/must select/,
82+
);
83+
});
84+
85+
test("builds minimal application claims without proposal contents", () => {
86+
const payload = buildActaOutcomeCredential("selected_provider", evidence());
87+
const serialized = JSON.stringify(payload);
88+
assert.match(serialized, /SubRosaSelectedProviderCredential/);
89+
assert.match(serialized, /selected_provider/);
90+
assert.equal(serialized.includes("proposal"), false);
91+
assert.equal(serialized.includes("price"), false);
92+
});
93+
94+
test("derives stable distinct credential IDs by round, subject, and outcome", async () => {
95+
const first = await actaOutcomeCredentialId({
96+
roundId: "42",
97+
subjectDid: SUBJECT,
98+
outcomeType: "selected_provider",
99+
});
100+
assert.equal(first, await actaOutcomeCredentialId({
101+
roundId: "42",
102+
subjectDid: SUBJECT,
103+
outcomeType: "selected_provider",
104+
}));
105+
assert.notEqual(first, await actaOutcomeCredentialId({
106+
roundId: "42",
107+
subjectDid: SUBJECT,
108+
outcomeType: "round_participation",
109+
}));
110+
assert.ok(first.length <= 64);
111+
});
112+
113+
test("uses ACTA idempotency on issuance and coalesces duplicate clicks", async () => {
114+
let identityCalls = 0;
115+
const requests: Array<{ headers: Headers; body: Record<string, unknown> }> = [];
116+
const client = {
117+
vaultVerify: async () => ({ status: "invalid" }),
118+
vaultGetVcDirect: async () => credential(),
119+
getIssuerIdentity: async () => ({ did: ISSUER }),
120+
getOrCreateIssuerIdentity: async () => {
121+
identityCalls += 1;
122+
return { did: ISSUER };
123+
},
124+
};
125+
const fetcher: typeof fetch = async (_input, init) => {
126+
const headers = new Headers(init?.headers);
127+
const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
128+
requests.push({ headers, body });
129+
return new Response(
130+
"signedXdr" in body
131+
? JSON.stringify({ tx_id: "real-acta-tx" })
132+
: JSON.stringify({ xdr: "unsigned-xdr", network: "Test SDF Network ; September 2015" }),
133+
{ status: 200, headers: { "Content-Type": "application/json" } },
134+
);
135+
};
136+
const adapter = new ActaAdapter({ apiKey: "runtime-key", client, fetcher });
137+
const issue = () => adapter.issueOutcomeCredential({
138+
outcomeType: "selected_provider",
139+
evidence: evidence(),
140+
owner: evidence().subjectWallet,
141+
issuer: `G${"E".repeat(55)}`,
142+
signTransaction: async () => "signed-xdr",
143+
});
144+
const [first, second] = await Promise.all([issue(), issue()]);
145+
assert.deepEqual(first, second);
146+
assert.equal(first.txId, "real-acta-tx");
147+
assert.equal(identityCalls, 1);
148+
assert.equal(requests.length, 2);
149+
assert.match(requests[1]!.headers.get("Idempotency-Key") ?? "", /^issue-sr-/);
150+
});
151+
152+
test("refresh-safe issuance returns the existing credential without another write", async () => {
153+
let fetchCalls = 0;
154+
const adapter = new ActaAdapter({
155+
apiKey: "runtime-key",
156+
client: {
157+
vaultVerify: async () => ({ status: "valid" }),
158+
vaultGetVcDirect: async () => credential(),
159+
getIssuerIdentity: async () => ({ did: ISSUER }),
160+
getOrCreateIssuerIdentity: async () => {
161+
throw new Error("must not create a new identity for an existing credential");
162+
},
163+
},
164+
fetcher: async () => {
165+
fetchCalls += 1;
166+
return new Response(null, { status: 500 });
167+
},
168+
});
169+
const result = await adapter.issueOutcomeCredential({
170+
outcomeType: "selected_provider",
171+
evidence: evidence(),
172+
owner: evidence().subjectWallet,
173+
issuer: `G${"E".repeat(55)}`,
174+
signTransaction: async () => "unused",
175+
});
176+
assert.equal(result.replayed, true);
177+
assert.equal(result.txId, null);
178+
assert.equal(fetchCalls, 0);
179+
});
180+
181+
test("surfaces missing ACTA configuration and verification failures", async () => {
182+
assert.throws(() => new ActaAdapter({ apiKey: "" }), /API key is required/);
183+
const adapter = new ActaAdapter({
184+
apiKey: "runtime-key",
185+
client: {
186+
vaultVerify: async () => {
187+
throw new Error("ACTA unavailable");
188+
},
189+
vaultGetVcDirect: async () => credential(),
190+
getIssuerIdentity: async () => null,
191+
getOrCreateIssuerIdentity: async () => ({ did: ISSUER }),
192+
},
193+
});
194+
await assert.rejects(
195+
() => adapter.verifyEligibility({
196+
policy: { credentialType: "VerifiedProviderCredential", trustedIssuerDid: ISSUER },
197+
owner: evidence().subjectWallet,
198+
credentialId: "provider-credential",
199+
}),
200+
/ACTA unavailable/,
201+
);
202+
});

0 commit comments

Comments
 (0)