Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ lifecycle lock. A provider result becomes usable after its native write succeeds
A failed write revokes the new grant. Refresh cannot change the user, organization,
issuer, client or resource. The caller supplies the provider implementation.

Offline status reads stored identity without provider calls. Logout requires
provider revocation before removing the native entry, then verifies local absence.
A failed revocation retains the entry for another attempt.

Run `pnpm run build`, `pnpm run typecheck`, and
`node --import tsx --test tests/auth/*.test.ts`.
Run `CLI_NATIVE_KEYRING_TEST=1 pnpm run test:native` with an available native
Expand Down
105 changes: 105 additions & 0 deletions tests/auth/workflow-access.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// @custom start
/** Stored OAuth lifecycle contracts. */
import assert from "node:assert/strict";
import test from "node:test";
import { accessTokenForCommand, status, CLIAuthWorkflowError } from "../../src/auth/workflow.js";
import {
commandSession as session,
memoryStore as store,
commandProvider as provider,
} from "./fixtures.js";

test("invariant workload overrides do not read or refresh OAuth storage", async () => {
let storeConstructions = 0;
let storedReads = 0;
let refreshes = 0;
const result = await status(
{ flags: { apiKey: "override-key" }, environment: {} },
() => {
storeConstructions += 1;
return {
...store(session()),
read: async () => {
storedReads += 1;
return session();
},
};
},
() =>
provider({
refresh: async (value) => {
refreshes += 1;
return value;
},
}),
false,
);

assert.deepEqual(result, { source: "flag" });
assert.equal(storeConstructions, 0);
assert.equal(storedReads, 0);
assert.equal(refreshes, 0);
});

test("invariant offline status returns local metadata without provider calls", async () => {
let providerCalls = 0;
const result = await status(
{ flags: {}, environment: {} },
() => store(session({ accessTokenExpiresAt: 1 })),
() => {
providerCalls += 1;
throw new Error("provider configuration must not be read");
},
true,
);

assert.equal(result.source, "oauth_session");
assert.equal(result.offline, true);
assert.equal(result.session.organizationId, "org_cli");
assert.equal(providerCalls, 0);
assert.equal(JSON.stringify(result).includes("oauth-access-token"), false);
});

test("invariant concurrent commands refresh one expired session once", async () => {
let refreshes = 0;
const serializedStore = store(session({ accessTokenExpiresAt: 1 }));
const authProvider = provider({
refresh: async () => {
refreshes += 1;
return session({ accessToken: "refreshed-access-token", accessTokenExpiresAt: 5_000_000 });
},
});

const tokens = await Promise.all([
accessTokenForCommand(serializedStore, authProvider, () => 1_000),
accessTokenForCommand(serializedStore, authProvider, () => 1_000),
]);

assert.deepEqual(tokens, ["refreshed-access-token", "refreshed-access-token"]);
assert.equal(refreshes, 1);
});

test("invariant refresh cannot silently change user or organization", async () => {
const original = session({ accessTokenExpiresAt: 1 });
const existing = store(original);

await assert.rejects(
accessTokenForCommand(
existing,
provider({ refresh: async () => session({ userId: "other_user" }) }),
() => 1_000,
),
(error) =>
error instanceof CLIAuthWorkflowError && error.code === "cli_session_identity_changed",
);
assert.deepEqual(await existing.read(), original);
});

test("invariant an issuer migration requires a fresh login", async () => {
await assert.rejects(
accessTokenForCommand(store(session()), provider({ issuer: "https://dedalus-as.example.com" })),
(error) =>
error instanceof CLIAuthWorkflowError && error.code === "cli_session_provider_mismatch",
);
});
// @custom end
140 changes: 140 additions & 0 deletions tests/auth/workflow-logout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// @custom start
/** Stored OAuth lifecycle contracts. */
import assert from "node:assert/strict";
import test from "node:test";
import { logout } from "../../src/auth/workflow.js";
import { CredentialStorageError } from "../../src/auth/credentials.js";
import {
commandSession as session,
memoryStore as store,
commandProvider as provider,
} from "./fixtures.js";

test("invariant logout removes local tokens after confirmed provider revocation", async () => {
const existing = store(session());
assert.deepEqual(await logout(existing, () => provider()), {
status: "logged_out",
});
assert.equal(await existing.read(), null);
});

test("invariant failed revocation preserves its error and credentials for retry", async () => {
const existing = store(session());
const failure = new Error("offline");
await assert.rejects(
logout(existing, () =>
provider({
revoke: async () => {
throw failure;
},
}),
),
(error) => error === failure,
);
assert.deepEqual(await existing.read(), session());
});

test("invariant logout preserves credentials when provider configuration is unavailable", async () => {
const existing = store(session());
const failure = new Error("invalid provider configuration");
await assert.rejects(
logout(existing, () => {
throw failure;
}),
(error) => error === failure,
);
assert.deepEqual(await existing.read(), session());
});

test("invariant logout never sends a session to a different provider", async () => {
const existing = store(session());
let revocations = 0;
const otherProvider = provider({
issuer: "https://dedalus-as.example.com",
clientId: "client_v2",
resource: "https://dcs.dedaluslabs.ai",
revoke: async () => {
revocations += 1;
},
});

await assert.rejects(
logout(existing, () => otherProvider),
{
code: "cli_session_provider_mismatch",
},
);
assert.equal(revocations, 0);
assert.deepEqual(await existing.read(), session());
});

test("invariant logout is idempotent without a local OAuth session", async () => {
assert.deepEqual(await logout(store(), () => provider()), {
status: "not_logged_in",
});
});

test("invariant logout cannot claim revocation of an unreadable credential", async () => {
let removed = false;
const obsolete = {
...store(),
read: async () => {
throw new CredentialStorageError("invalid_credential");
},
remove: async () => {
removed = true;
return true;
},
};

await assert.rejects(
logout(obsolete, () => provider()),
{ code: "invalid_credential" },
);
assert.equal(removed, false);
});

test("invariant logout verifies local absence after acknowledged deletion", async () => {
for (const removed of [false, true]) {
await assert.rejects(
logout({ ...store(session()), remove: async () => removed }, () => provider()),
{ code: "cli_credential_store_failed" },
);
}
});

test("invariant logout preserves cleanup and verification failures", async () => {
const failure = new Error("keyring locked");
await assert.rejects(
logout(
{
...store(session()),
remove: async () => {
throw failure;
},
},
() => provider(),
),
(error) => error === failure,
);

let removed = false;
await assert.rejects(
logout(
{
...store(session()),
read: async () => {
if (removed) throw failure;
return session();
},
remove: async () => {
removed = true;
return true;
},
},
() => provider(),
),
(error) => error === failure,
);
});
// @custom end