feat(platform): add Discord onboarding and Velocity gate

This commit is contained in:
dmg
2026-08-01 13:45:18 -04:00
parent 9d305e5dc9
commit c5de0a1810
80 changed files with 4840 additions and 1572 deletions
+104
View File
@@ -0,0 +1,104 @@
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import {
createMagicLink,
exchangeMagicLink,
InvalidLoginCodeError,
type AuthRepository,
type PendingLoginCode,
} from "../src/index";
class MemoryAuthRepository implements AuthRepository {
loginCode: PendingLoginCode | undefined;
consumedHash: string | undefined;
async saveLoginCode(code: PendingLoginCode) {
this.loginCode = code;
}
async exchangeLoginCode(input: Parameters<AuthRepository["exchangeLoginCode"]>[0]) {
this.consumedHash = input.loginCodeHash;
if (
!this.loginCode ||
this.loginCode.tokenHash !== input.loginCodeHash ||
this.loginCode.expiresAt <= input.now
) {
return null;
}
this.loginCode = undefined;
return {
user: {
id: "01JQ0000000000000000000000",
discordUserId: "123456789012345678",
discordUsername: "steve",
firstName: null,
},
isNewUser: true,
};
}
}
const identity = {
id: "123456789012345678",
username: "steve",
globalName: "Steve",
};
const now = new Date("2026-08-01T12:00:00.000Z");
function hash(value: string) {
return createHash("sha256").update(value).digest("hex");
}
describe("Discord magic-link authentication", () => {
it("returns a link while persisting only the token hash", async () => {
const repository = new MemoryAuthRepository();
const result = await createMagicLink(identity, {
repository,
appUrl: "https://accounts.example.com",
now: () => now,
randomToken: () => "private-login-token",
});
expect(result.url).toBe("https://accounts.example.com/auth/discord?code=private-login-token");
expect(result.expiresAt).toEqual(new Date("2026-08-01T12:10:00.000Z"));
expect(repository.loginCode).toMatchObject({
tokenHash: hash("private-login-token"),
discordUserId: identity.id,
discordUsername: identity.username,
});
expect(JSON.stringify(repository.loginCode)).not.toContain("private-login-token");
});
it("exchanges a valid one-time code for a session", async () => {
const repository = new MemoryAuthRepository();
await createMagicLink(identity, {
repository,
appUrl: "https://accounts.example.com",
now: () => now,
randomToken: () => "private-login-token",
});
const result = await exchangeMagicLink("private-login-token", {
repository,
now: () => new Date("2026-08-01T12:01:00.000Z"),
randomToken: () => "private-session-token",
});
expect(result.sessionToken).toBe("private-session-token");
expect(result.user.discordUserId).toBe(identity.id);
expect(repository.consumedHash).toBe(hash("private-login-token"));
});
it("rejects an expired or already-consumed code", async () => {
const repository = new MemoryAuthRepository();
await expect(
exchangeMagicLink("missing-token", {
repository,
now: () => now,
randomToken: () => "private-session-token",
}),
).rejects.toBeInstanceOf(InvalidLoginCodeError);
});
});
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { extractOidcRoles } from "../src/index";
function unsignedToken(payload: Record<string, unknown>) {
return `header.${Buffer.from(JSON.stringify(payload)).toString("base64url")}.signature`;
}
describe("OIDC role extraction", () => {
it("combines realm and configured-client roles from Keycloak tokens", () => {
const roles = extractOidcRoles({
clientId: "minecraft-account-manager-admin",
profile: { groups: ["support"] },
accessToken: unsignedToken({
realm_access: { roles: ["minecraft-account-manager-admin"] },
resource_access: {
"minecraft-account-manager-admin": { roles: ["settings-editor"] },
},
}),
});
expect(roles).toEqual([
"support",
"minecraft-account-manager-admin",
"settings-editor",
]);
});
it("treats malformed token payloads as having no roles", () => {
expect(extractOidcRoles({ clientId: "admin", accessToken: "invalid" })).toEqual([]);
});
});
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { hashToken, isRequestTimestampFresh, verifyHashedToken } from "../src/index";
describe("plugin request authentication", () => {
it("compares an opaque token with its stored hash", () => {
const storedHash = hashToken("correct-high-entropy-token");
expect(verifyHashedToken("correct-high-entropy-token", storedHash)).toBe(true);
expect(verifyHashedToken("wrong-token", storedHash)).toBe(false);
expect(verifyHashedToken("correct-high-entropy-token", "malformed")).toBe(false);
});
it("rejects stale and excessively future-dated requests", () => {
const now = new Date("2026-08-01T12:00:00.000Z");
expect(isRequestTimestampFresh(new Date("2026-08-01T11:59:30.000Z"), now, 45_000)).toBe(true);
expect(isRequestTimestampFresh(new Date("2026-08-01T11:59:14.000Z"), now, 45_000)).toBe(false);
expect(isRequestTimestampFresh(new Date("2026-08-01T12:00:46.000Z"), now, 45_000)).toBe(false);
});
});