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 packages/core/src/component/_generated/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/src/component/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
REFRESH_GRACE_MS,
SPENT_TOKEN_HORIZON_MS,
MAX_SESSIONS_PER_USER,
} from "./public.ts";
} from "./sessions.ts";

/**
* The component's modules, and the test-only `testApp` module. The fixture file
Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/component/expiry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Token lifetimes: the validated config of a call, and when a refresh token
// expires under it.

import type { Doc } from "./_generated/dataModel.ts";
import {
DEFAULT_ACCESS_TOKEN_TTL_SECONDS,
DEFAULT_REFRESH_TOKEN_TTL_SECONDS,
} from "../lib/ttl.ts";

// `ctx.auth.signJwt` refuses to mint a token that lives longer than an hour,
// so an access-token TTL past that is a misconfiguration.
const MAX_ACCESS_TOKEN_TTL_SECONDS = 60 * 60; // 1 hour

/**
* The token lifetimes for a call, with the app's overrides applied over the
* defaults. Validated so a misconfiguration fails loudly rather than minting
* nonsensical sessions.
*/
export type TtlConfig = {
accessTokenTtlSeconds: number;
refreshTokenTtlSeconds: number;
};

export function resolveTtlConfig(args: {
accessTokenTtlSeconds?: number;
refreshTokenTtlSeconds?: number;
}): TtlConfig {
const accessTokenTtlSeconds =
args.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
const refreshTokenTtlSeconds =
args.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS;
if (accessTokenTtlSeconds <= 0 || refreshTokenTtlSeconds <= 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: NaN and Infinity pass this check, and a non-finite refresh TTL makes the refresh expiry comparison always false. Reject non-finite TTLs here with Number.isFinite so misconfiguration cannot create non-expiring sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/component/expiry.ts, line 32:

<comment>`NaN` and `Infinity` pass this check, and a non-finite refresh TTL makes the refresh expiry comparison always false. Reject non-finite TTLs here with `Number.isFinite` so misconfiguration cannot create non-expiring sessions.</comment>

<file context>
@@ -0,0 +1,61 @@
+    args.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
+  const refreshTokenTtlSeconds =
+    args.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS;
+  if (accessTokenTtlSeconds <= 0 || refreshTokenTtlSeconds <= 0) {
+    throw new Error("Token TTLs must be positive.");
+  }
</file context>
Suggested change
if (accessTokenTtlSeconds <= 0 || refreshTokenTtlSeconds <= 0) {
if (
!Number.isFinite(accessTokenTtlSeconds) ||
!Number.isFinite(refreshTokenTtlSeconds) ||
accessTokenTtlSeconds <= 0 ||
refreshTokenTtlSeconds <= 0
) {

throw new Error("Token TTLs must be positive.");
}
if (accessTokenTtlSeconds > MAX_ACCESS_TOKEN_TTL_SECONDS) {
throw new Error(
"Access-token TTL must be at most one hour (the deployment refuses " +
"to sign a longer-lived JWT).",
);
}
if (accessTokenTtlSeconds >= refreshTokenTtlSeconds) {
throw new Error(
"Access-token TTL must be shorter than the refresh-token TTL.",
);
}
return { accessTokenTtlSeconds, refreshTokenTtlSeconds };
}

/**
* When a refresh token expires under the given refresh-token TTL.
*
* Expiry is derived rather than stored, so a changed TTL applies to every
* outstanding token. It also means a session's newest token is always its
* last to expire.
*/
export function refreshTokenExpiresAt(
token: Pick<Doc<"refreshTokens">, "_creationTime">,
refreshTokenTtlSeconds: number,
): number {
return token._creationTime + refreshTokenTtlSeconds * 1000;
}
Loading
Loading