@jkaibuildiq/bridge-contracts
v1.1.0
Published
Shared JWT contracts, helpers, and TypeScript types for the aiiq.world <-> predict.aiiq.world bridge (SSO + cross-platform wallet + activity feed)
Maintainers
Readme
@jkaibuildiq/bridge-contracts
Shared JWT contracts, helpers, and TypeScript types for the aiiq.world ⇄ predict.aiiq.world bridge:
- SSO tokens (aiiq → predict, so signed-in aiiq users land on predict already authed)
- Service tokens (predict ⇄ aiiq, for wallet transfers + cross-platform activity feed)
- The canonical type definitions (
TransferRequest,TransferResponse,ActivityItem, etc.) both apps depend on
Both repos must run against the same version. A drift between sides
manifests as silent Invalid service token / Invalid or expired token
errors with no other signal — exactly the class of bug this package
exists to prevent.
Install
npm install @jkaibuildiq/bridge-contracts
# or
pnpm add @jkaibuildiq/bridge-contractsjose is a peer dependency (any ^5 or ^6). Both consumer apps already
have it; if you're starting fresh:
pnpm add joseWhat's exported
Constants (./constants)
import {
PREDICT_ORIGIN,
SSO_ISSUER, // "aiiq.world"
SSO_AUDIENCE, // "predict.aiiq.world"
SERVICE_ISSUER, // "predict.aiiq.world"
SERVICE_AUDIENCE, // "aiiq.world"
SERVICE_SCOPES, // { WALLET_TRANSFER, WALLET_READ }
MIN_SECRET_LENGTH, // 32
DEFAULT_SSO_TTL, // "30s"
DEFAULT_SERVICE_TTL, // "60s"
} from "@jkaibuildiq/bridge-contracts";Types (./types)
import type {
BridgeUser,
BridgeTokenPayload,
ServiceTokenPayload,
TransferAction,
TransferRequest,
TransferResponse,
TransferSuccess,
TransferFailure,
ActivityItem,
ActivityType,
ActivityResponse,
} from "@jkaibuildiq/bridge-contracts";SSO helpers
import {
createBridgeToken,
verifyBridgeToken,
buildPredictUrl,
} from "@jkaibuildiq/bridge-contracts";Service-token helpers
import {
createServiceToken,
verifyServiceToken,
} from "@jkaibuildiq/bridge-contracts";Usage — aiiq.world side (token producer for SSO, verifier for service tokens)
Mint an SSO token + redirect
// src/app/api/auth/bridge-redirect/route.ts
import { NextRequest, NextResponse } from "next/server";
import { validateSession } from "@/lib/auth";
import { createBridgeToken, buildPredictUrl } from "@jkaibuildiq/bridge-contracts";
export async function GET(request: NextRequest) {
const secret = process.env.BRIDGE_JWT_SECRET!;
const session = await validateSession(/* ... */);
if (!session) {
return NextResponse.redirect("https://predict.aiiq.world/dashboard");
}
const token = await createBridgeToken(secret, {
id: session.user.id,
email: session.user.email,
username: session.user.username,
});
return NextResponse.redirect(buildPredictUrl(token, "/dashboard"));
}Verify a service token (wallet bridge endpoint)
// src/app/api/aoa/wallet/bridge-transfer/route.ts
import { verifyServiceToken, SERVICE_SCOPES } from "@jkaibuildiq/bridge-contracts";
const auth = req.headers.get("authorization");
if (!auth?.startsWith("Bearer ")) return Response.json(..., { status: 401 });
try {
await verifyServiceToken(
process.env.BRIDGE_JWT_SECRET!,
auth.slice(7),
SERVICE_SCOPES.WALLET_TRANSFER,
);
} catch {
return Response.json(..., { status: 403 });
}Usage — predict.aiiq.world side (verifier for SSO, producer for service tokens)
Verify an inbound SSO token
// src/app/api/auth/bridge/route.ts
import { verifyBridgeToken } from "@jkaibuildiq/bridge-contracts";
const { token } = await req.json();
const payload = await verifyBridgeToken(process.env.BRIDGE_JWT_SECRET!, token);
// payload: { sub: <aiiq user id>, email, username, iss, aud, iat, exp }
const externalUserId = payload.sub;
const externalEmail = payload.email?.toLowerCase().trim();
const externalUsername = payload.username.trim();Mint a service token + call aiiq's wallet bridge
import {
createServiceToken,
SERVICE_SCOPES,
type TransferRequest,
type TransferResponse,
} from "@jkaibuildiq/bridge-contracts";
async function callMainBridge(payload: TransferRequest): Promise<TransferResponse> {
const token = await createServiceToken(process.env.BRIDGE_JWT_SECRET!, {
scope: SERVICE_SCOPES.WALLET_TRANSFER,
ref: payload.reference,
});
const res = await fetch(
"https://aiiq.world/api/aoa/wallet/bridge-transfer",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
}
);
return (await res.json()) as TransferResponse;
}Mint a service token + read aiiq activity
import {
createServiceToken,
SERVICE_SCOPES,
type ActivityResponse,
} from "@jkaibuildiq/bridge-contracts";
const token = await createServiceToken(process.env.BRIDGE_JWT_SECRET!, {
scope: SERVICE_SCOPES.WALLET_READ,
ref: `dashboard-activity-${userId}`,
});
const url = new URL("https://aiiq.world/api/aoa/bridge/activity");
url.searchParams.set("externalUserId", aiiqUserId);
url.searchParams.set("limit", "25");
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (res.status === 404) {
// endpoint not yet deployed — silently degrade
} else if (!res.ok) {
// 401/403/500 — surface as "aiiq activity unavailable"
} else {
const data = (await res.json()) as ActivityResponse;
// data.items: ActivityItem[]
}Wire reference
SSO token (aiiq → predict)
header: { alg: "HS256", typ: "JWT" }
payload: {
iss: "aiiq.world",
aud: "predict.aiiq.world",
sub: <aiiq user UUID>,
email: <string | undefined>,
username: <string>,
iat: <unix>,
exp: <iat + 30s>,
}Service token (predict → aiiq)
header: { alg: "HS256", typ: "JWT" }
payload: {
iss: "predict.aiiq.world",
aud: "aiiq.world",
scope: "wallet_transfer" | "wallet_read" | "position_read",
ref: <correlation id>,
iat: <unix>,
exp: <iat + 60s>,
}Widget token (aiiq → predict, v1.1+)
header: { alg: "HS256", typ: "JWT" }
payload: {
iss: "aiiq.world",
aud: "predict.aiiq.world",
scope: "widget_proxy", // always — bound inside createWidgetToken
ref: <correlation id>, // required — per-request crypto.randomUUID()
iat: <unix>,
exp: <iat + 60s>,
}Changelog
1.1.0 — additive, forward-compatible with 1.0
createWidgetToken/verifyWidgetToken— bidirectional widget bridge primitive for the aiiq → predict direction (existingcreateServiceToken/verifyServiceTokengo predict → aiiq and stay unchanged). Aiiq mints, predict verifies. Scope is hardcoded toSERVICE_SCOPES.WIDGET_PROXYinside both functions — caller cannot mint with the wrong scope, verifier cannot accept the wrong scope.refis required on verify (predict's widget audit log keys on it). Backs the four widget bridge endpoints (widget-snapshot/widget-trade/widget-balance/widget-telemetry) — see the spec in aoa-predictdocs/aiiq-widget-spec.md/ age-of-aidocs/widget-implementation.md.WIDGET_ISSUER/WIDGET_AUDIENCEconstants. Hostname values happen to equalSSO_ISSUER/SSO_AUDIENCEtoday but are kept distinct so the wire-format intent stays self-documenting if either ever moves origin.SERVICE_SCOPES.POSITION_READ = "position_read"for a futureGET /api/aoa/bridge/positionsendpoint. Reserved under Option B (aiiq gameplay is in-memory; the endpoint returns{ positions: [] }until aiiq grows a real-money trading surface).SERVICE_SCOPES.WIDGET_PROXY = "widget_proxy"for the cross-site QuickPredict widget mounted on aiiq.world. Backs the aiiq proxy routes and the matching predict-sidewidget-*bridge endpoints. Carved out ofwallet_transferso it can be revoked independently and rate-limited differently. Wire shapes for the widget request/response payloads themselves are deferred to v1.2 once the implementation settles.ACTIVITY_TYPESadds"REFUND"and"RAKE". Predict-emitted on its own side; aiiq doesn't emit these because aiiq has no persisted gameplay to refund or rake from.- New
AiiqPosition+PositionsResponsetypes for the same future endpoint.
No removals, no shape changes — 1.0.0 consumers continue to work
unchanged.
1.0.0 — initial release
Issuer / audience / scope strings, token shapes, TransferRequest /
TransferResponse / ActivityItem / ActivityResponse types,
createBridgeToken / verifyBridgeToken / createServiceToken /
verifyServiceToken helpers, buildPredictUrl.
Versioning
- Patch (1.0.x) — internal helper refactors, doc fixes, no wire changes.
- Minor (1.x.0) — additive new types / helpers (e.g. new scope, new activity type). Backwards compatible.
- Major (x.0.0) — wire-breaking changes (issuer/audience/scope renames, token shape changes). Coordinated deploy required.
License
MIT
