391 lines
13 KiB
TypeScript
391 lines
13 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// Pure-function copies of helpers from claudeAuthImport.ts — no DB deps pulled in.
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function toRecord(value: unknown): JsonRecord {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
|
}
|
|
|
|
function toNonEmptyString(value: unknown): string | null {
|
|
if (typeof value !== "string") return null;
|
|
const trimmed = value.trim();
|
|
return trimmed ? trimmed : null;
|
|
}
|
|
|
|
class ClaudeAuthFileError extends Error {
|
|
status: number;
|
|
code: string;
|
|
constructor(message: string, status = 400, code = "invalid_request") {
|
|
super(message);
|
|
this.name = "ClaudeAuthFileError";
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
interface ParsedClaudeAuth {
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
expiresAt: string | null;
|
|
scopes: string[];
|
|
subscriptionType: string | null;
|
|
rateLimitTier: string | null;
|
|
email: string | null;
|
|
}
|
|
|
|
interface EnrichedClaudeAuth extends ParsedClaudeAuth {
|
|
accountUUID: string | null;
|
|
organizationUUID: string | null;
|
|
organizationName: string | null;
|
|
organizationType: string | null;
|
|
rateLimitTier: string | null;
|
|
}
|
|
|
|
interface CreateConnectionOptions {
|
|
name?: string;
|
|
email?: string;
|
|
overwriteExisting?: boolean;
|
|
}
|
|
|
|
function parseClaudeAuth(
|
|
raw: unknown
|
|
): ParsedClaudeAuth | { error: string; code: string; status: number } {
|
|
const doc = toRecord(raw);
|
|
const oauthBlock = toRecord(doc.claudeAiOauth);
|
|
|
|
const accessToken = toNonEmptyString(oauthBlock.accessToken);
|
|
const refreshToken = toNonEmptyString(oauthBlock.refreshToken);
|
|
|
|
if (!accessToken) {
|
|
return {
|
|
error: "accessToken is missing or empty in claudeAiOauth",
|
|
code: "missing_access_token",
|
|
status: 400,
|
|
};
|
|
}
|
|
|
|
if (!refreshToken) {
|
|
return {
|
|
error: "refreshToken is missing or empty in claudeAiOauth",
|
|
code: "missing_refresh_token",
|
|
status: 400,
|
|
};
|
|
}
|
|
|
|
let expiresAt: string | null = null;
|
|
const rawExpiresAt = oauthBlock.expiresAt;
|
|
if (typeof rawExpiresAt === "number" && Number.isFinite(rawExpiresAt)) {
|
|
expiresAt = new Date(rawExpiresAt).toISOString();
|
|
} else if (typeof rawExpiresAt === "string" && rawExpiresAt.trim()) {
|
|
expiresAt = rawExpiresAt.trim();
|
|
}
|
|
|
|
const rawScopes = oauthBlock.scopes;
|
|
const scopes: string[] = Array.isArray(rawScopes)
|
|
? rawScopes.filter((s): s is string => typeof s === "string")
|
|
: [];
|
|
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
expiresAt,
|
|
scopes,
|
|
subscriptionType: toNonEmptyString(oauthBlock.subscriptionType),
|
|
rateLimitTier: toNonEmptyString(oauthBlock.rateLimitTier),
|
|
email: null,
|
|
};
|
|
}
|
|
|
|
// Mirror of the duplicate-detection + identity-check logic in createConnectionFromAuthFile
|
|
function checkCreateConnectionPreconditions(
|
|
enriched: EnrichedClaudeAuth,
|
|
options: CreateConnectionOptions,
|
|
existingByAccountUUID: JsonRecord | null
|
|
): { error: string; code: string; status: number } | null {
|
|
if (enriched.accountUUID && existingByAccountUUID && !options.overwriteExisting) {
|
|
return {
|
|
error:
|
|
"A Claude connection for this account already exists. Pass overwriteExisting: true to replace it.",
|
|
code: "duplicate_account",
|
|
status: 409,
|
|
};
|
|
}
|
|
|
|
if (!enriched.email && !enriched.accountUUID && !options.overwriteExisting) {
|
|
return {
|
|
error:
|
|
"Could not verify the account identity (bootstrap failed and no email/accountUUID available). Pass overwriteExisting: true to import anyway.",
|
|
code: "identity_unverified",
|
|
status: 409,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ──── Tests: parseClaudeAuth ──────────────────────────────────────────────────
|
|
|
|
test("parseClaudeAuth: valid payload returns all fields", () => {
|
|
const raw = {
|
|
claudeAiOauth: {
|
|
accessToken: "at-abc",
|
|
refreshToken: "rt-xyz",
|
|
expiresAt: 1768527451123,
|
|
scopes: ["user:inference"],
|
|
subscriptionType: "pro",
|
|
rateLimitTier: "default",
|
|
},
|
|
};
|
|
const result = parseClaudeAuth(raw);
|
|
assert.ok(!("error" in result));
|
|
const parsed = result as ParsedClaudeAuth;
|
|
assert.equal(parsed.accessToken, "at-abc");
|
|
assert.equal(parsed.refreshToken, "rt-xyz");
|
|
assert.ok(parsed.expiresAt !== null, "expiresAt should be non-null");
|
|
assert.deepEqual(parsed.scopes, ["user:inference"]);
|
|
assert.equal(parsed.subscriptionType, "pro");
|
|
assert.equal(parsed.rateLimitTier, "default");
|
|
});
|
|
|
|
test("parseClaudeAuth: rejects empty accessToken", () => {
|
|
const result = parseClaudeAuth({ claudeAiOauth: { accessToken: "", refreshToken: "rt" } });
|
|
assert.ok("error" in result);
|
|
assert.equal((result as { code: string }).code, "missing_access_token");
|
|
assert.equal((result as { status: number }).status, 400);
|
|
});
|
|
|
|
test("parseClaudeAuth: rejects missing accessToken", () => {
|
|
const result = parseClaudeAuth({ claudeAiOauth: { refreshToken: "rt" } });
|
|
assert.ok("error" in result);
|
|
assert.equal((result as { code: string }).code, "missing_access_token");
|
|
});
|
|
|
|
test("parseClaudeAuth: rejects empty refreshToken", () => {
|
|
const result = parseClaudeAuth({ claudeAiOauth: { accessToken: "at", refreshToken: " " } });
|
|
assert.ok("error" in result);
|
|
assert.equal((result as { code: string }).code, "missing_refresh_token");
|
|
assert.equal((result as { status: number }).status, 400);
|
|
});
|
|
|
|
test("parseClaudeAuth: converts expiresAt ms number to ISO string", () => {
|
|
const msEpoch = 1768527451123;
|
|
const result = parseClaudeAuth({
|
|
claudeAiOauth: { accessToken: "at", refreshToken: "rt", expiresAt: msEpoch },
|
|
});
|
|
assert.ok(!("error" in result));
|
|
const parsed = result as ParsedClaudeAuth;
|
|
assert.ok(parsed.expiresAt !== null);
|
|
assert.equal(parsed.expiresAt, new Date(msEpoch).toISOString());
|
|
});
|
|
|
|
test("parseClaudeAuth: scopes absent produces empty array", () => {
|
|
const result = parseClaudeAuth({ claudeAiOauth: { accessToken: "at", refreshToken: "rt" } });
|
|
assert.ok(!("error" in result));
|
|
assert.deepEqual((result as ParsedClaudeAuth).scopes, []);
|
|
});
|
|
|
|
test("parseClaudeAuth: non-string scopes are filtered out", () => {
|
|
const result = parseClaudeAuth({
|
|
claudeAiOauth: { accessToken: "at", refreshToken: "rt", scopes: ["user:inference", 42, null] },
|
|
});
|
|
assert.ok(!("error" in result));
|
|
assert.deepEqual((result as ParsedClaudeAuth).scopes, ["user:inference"]);
|
|
});
|
|
|
|
test("parseClaudeAuth: non-object input returns missing_access_token", () => {
|
|
const result = parseClaudeAuth("not an object");
|
|
assert.ok("error" in result);
|
|
assert.equal((result as { code: string }).code, "missing_access_token");
|
|
});
|
|
|
|
test("parseClaudeAuth: null input returns missing_access_token", () => {
|
|
const result = parseClaudeAuth(null);
|
|
assert.ok("error" in result);
|
|
assert.equal((result as { code: string }).code, "missing_access_token");
|
|
});
|
|
|
|
// ──── Tests: enrichWithBootstrap (simulated) ──────────────────────────────────
|
|
|
|
test("enrichWithBootstrap success: extracts accountUUID and account_email", () => {
|
|
// Simulate a successful bootstrap response
|
|
const bootstrapBody = {
|
|
account_uuid: "uuid-123",
|
|
organization_uuid: "org-uuid-456",
|
|
organization_name: "Acme Corp",
|
|
organization_type: "enterprise",
|
|
rate_limit_tier: "premium",
|
|
account_email: "alice@example.com",
|
|
};
|
|
|
|
const parsed: ParsedClaudeAuth = {
|
|
accessToken: "at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: null,
|
|
};
|
|
|
|
// Simulate what enrichWithBootstrap does with a successful body
|
|
const enriched: EnrichedClaudeAuth = {
|
|
...parsed,
|
|
accountUUID: toNonEmptyString(bootstrapBody.account_uuid),
|
|
organizationUUID: toNonEmptyString(bootstrapBody.organization_uuid),
|
|
organizationName: toNonEmptyString(bootstrapBody.organization_name),
|
|
organizationType: toNonEmptyString(bootstrapBody.organization_type),
|
|
rateLimitTier: toNonEmptyString(bootstrapBody.rate_limit_tier),
|
|
email: toNonEmptyString(bootstrapBody.account_email),
|
|
};
|
|
|
|
assert.equal(enriched.accountUUID, "uuid-123");
|
|
assert.equal(enriched.email, "alice@example.com");
|
|
assert.equal(enriched.organizationName, "Acme Corp");
|
|
assert.equal(enriched.rateLimitTier, "premium");
|
|
});
|
|
|
|
test("enrichWithBootstrap 401: returns null fields, does not throw", () => {
|
|
// Simulate a 401 response (non-ok) — enrichWithBootstrap returns base with null fields
|
|
const parsed: ParsedClaudeAuth = {
|
|
accessToken: "expired-at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: null,
|
|
};
|
|
|
|
const enriched: EnrichedClaudeAuth = {
|
|
...parsed,
|
|
accountUUID: null,
|
|
organizationUUID: null,
|
|
organizationName: null,
|
|
organizationType: null,
|
|
rateLimitTier: null,
|
|
};
|
|
|
|
assert.equal(enriched.accountUUID, null);
|
|
assert.equal(enriched.email, null);
|
|
// Must not throw — callers rely on null fields being a valid enrichment result
|
|
assert.ok(true, "no throw occurred");
|
|
});
|
|
|
|
test("enrichWithBootstrap timeout: returns null fields, does not throw", () => {
|
|
// Simulate timeout path — same as 401 result shape
|
|
const parsed: ParsedClaudeAuth = {
|
|
accessToken: "at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: null,
|
|
};
|
|
|
|
const enriched: EnrichedClaudeAuth = {
|
|
...parsed,
|
|
accountUUID: null,
|
|
organizationUUID: null,
|
|
organizationName: null,
|
|
organizationType: null,
|
|
rateLimitTier: parsed.rateLimitTier,
|
|
};
|
|
|
|
assert.equal(enriched.accountUUID, null);
|
|
assert.ok(true, "timeout path returns gracefully without throw");
|
|
});
|
|
|
|
// ──── Tests: createConnectionFromAuthFile preconditions ───────────────────────
|
|
|
|
test("createConnectionFromAuthFile: throws 409 duplicate_account when existing + overwrite=false", () => {
|
|
const enriched: EnrichedClaudeAuth = {
|
|
accessToken: "at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: "alice@example.com",
|
|
accountUUID: "uuid-123",
|
|
organizationUUID: null,
|
|
organizationName: null,
|
|
organizationType: null,
|
|
};
|
|
|
|
const existingConnection: JsonRecord = { id: "conn-1", name: "Alice" };
|
|
const options: CreateConnectionOptions = { overwriteExisting: false };
|
|
|
|
const result = checkCreateConnectionPreconditions(enriched, options, existingConnection);
|
|
assert.ok(result !== null);
|
|
assert.equal(result!.code, "duplicate_account");
|
|
assert.equal(result!.status, 409);
|
|
});
|
|
|
|
test("createConnectionFromAuthFile: throws 409 identity_unverified when no email + no accountUUID + overwrite=false", () => {
|
|
const enriched: EnrichedClaudeAuth = {
|
|
accessToken: "at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: null,
|
|
accountUUID: null,
|
|
organizationUUID: null,
|
|
organizationName: null,
|
|
organizationType: null,
|
|
};
|
|
|
|
const options: CreateConnectionOptions = { overwriteExisting: false };
|
|
const result = checkCreateConnectionPreconditions(enriched, options, null);
|
|
assert.ok(result !== null);
|
|
assert.equal(result!.code, "identity_unverified");
|
|
assert.equal(result!.status, 409);
|
|
});
|
|
|
|
test("createConnectionFromAuthFile: allows create without email/accountUUID when overwrite=true", () => {
|
|
const enriched: EnrichedClaudeAuth = {
|
|
accessToken: "at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: null,
|
|
accountUUID: null,
|
|
organizationUUID: null,
|
|
organizationName: null,
|
|
organizationType: null,
|
|
};
|
|
|
|
const options: CreateConnectionOptions = { overwriteExisting: true };
|
|
const result = checkCreateConnectionPreconditions(enriched, options, null);
|
|
assert.equal(result, null, "no precondition error when overwriteExisting=true");
|
|
});
|
|
|
|
test("createConnectionFromAuthFile: allows create when existing but overwrite=true", () => {
|
|
const enriched: EnrichedClaudeAuth = {
|
|
accessToken: "at",
|
|
refreshToken: "rt",
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
rateLimitTier: null,
|
|
email: "alice@example.com",
|
|
accountUUID: "uuid-123",
|
|
organizationUUID: null,
|
|
organizationName: null,
|
|
organizationType: null,
|
|
};
|
|
|
|
const existingConnection: JsonRecord = { id: "conn-1" };
|
|
const options: CreateConnectionOptions = { overwriteExisting: true };
|
|
|
|
const result = checkCreateConnectionPreconditions(enriched, options, existingConnection);
|
|
assert.equal(result, null, "no precondition error when overwriteExisting=true");
|
|
});
|