Add Ghostlight Dungeon Heimdall profile

This commit is contained in:
Meta Vens 2026-08-19 00:39:09 +02:00
commit 210e16220f
5 changed files with 56 additions and 3 deletions

View file

@ -0,0 +1,25 @@
# Ghostlight Dungeon Access Profile
Ghostlight Dungeon reuses Heimdall's browser-completion Discord sign-in path.
Heimdall owns provider OAuth, local account identity, signed access claims, and
refresh. Ghostlight verifies claims locally and owns campaigns, campaign
membership, current campaign selection, and every world mutation.
## App identity
- `app_slug`: `ghostlight`
- public URL: `https://yggdrasil.gamecult.org/ghostlight/`
- identity provider: Discord
- public-demo rule: any Heimdall-authenticated Discord identity receives
`app_access`; no GameCult guild role is required
## Capabilities
- `app_access`: shared capability granting entry to the demo
- `campaign_play`: hybrid capability; Ghostlight must also prove that the
authenticated account owns the selected campaign
The browser never carries provider tokens. It receives a one-time completion
code, Ghostlight redeems it server-to-server, verifies the resulting
`aud=ghostlight` claim against Heimdall JWKS, and establishes app-local campaign
authority.

View file

@ -197,11 +197,38 @@ const bifrostProfile: AppProfile = {
},
};
const ghostlightProfile: AppProfile = {
slug: "ghostlight",
displayName: "Ghostlight Dungeon",
profileVersion: "2026-08-19",
identityProviders: ["discord"],
entitlementSources: [],
managedConnectionProviders: [],
capabilities: [
{
key: "app_access",
mode: "shared",
summary: "May enter the public Ghostlight Dungeon demo.",
sharedRule: identityFacts.authenticated,
},
{
key: "campaign_play",
mode: "hybrid",
summary: "May play campaigns owned by the authenticated Ghostlight account.",
localRequirement: "Ghostlight must combine app_access with local campaign ownership.",
},
],
evaluateSharedCapabilities(context) {
return context.facts.has(identityFacts.authenticated) ? ["app_access"] : [];
},
};
export const appProfiles: Record<AppSlug, AppProfile> = {
repixelizer: repixelizerProfile,
streampixels: streampixelsProfile,
spotiverse: spotiverseProfile,
bifrost: bifrostProfile,
ghostlight: ghostlightProfile,
};
export function getAppProfile(appSlug: AppSlug): AppProfile {

View file

@ -2,7 +2,7 @@ export const providers = ["discord", "patreon", "github", "twitch", "youtube", "
export type Provider = (typeof providers)[number];
export const appSlugs = ["repixelizer", "streampixels", "spotiverse", "bifrost"] as const;
export const appSlugs = ["repixelizer", "streampixels", "spotiverse", "bifrost", "ghostlight"] as const;
export type AppSlug = (typeof appSlugs)[number];

View file

@ -37,7 +37,7 @@ describe("Heimdall verse runtime publication", () => {
runtime: {
storageBackend: "postgres",
configuredProviders: ["discord", "twitch"],
appProfileCount: 4,
appProfileCount: 5,
},
});
expect(bySchema.get("heimdall.command_boundary.v1")).toMatchObject({
@ -56,7 +56,7 @@ describe("Heimdall verse runtime publication", () => {
expect(bySchema.get("idunn.daemon_health")).toEqual([
"yggdrasil-heimdall",
"active",
"Heimdall auth runtime active; storage=postgres; providers=2/6; apps=4; healthTransport=CultNet/RUDP",
"Heimdall auth runtime active; storage=postgres; providers=2/6; apps=5; healthTransport=CultNet/RUDP",
"2026-06-16T17:10:00.000Z",
"heimdall.cultnet-rudp-provider-health",
"daemon-published",

View file

@ -37,6 +37,7 @@ describe("Heimdall Verse witness advertisement", () => {
"streampixels",
"spotiverse",
"bifrost",
"ghostlight",
]);
expect(advertisement.documents.map((document) => document.schemaId)).toEqual([...heimdallWitnessSchemaIds]);
});