forked from bai/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
@@ -15,28 +15,41 @@ export const DEFAULT_SESSION_TTL_SECONDS = 7 * 24 * 60 * 60;
|
||||
/** OAuth state cookie TTL: 10 minutes. */
|
||||
export const OAUTH_STATE_TTL_SECONDS = 10 * 60;
|
||||
|
||||
export interface SessionPayload {
|
||||
readonly userId: string;
|
||||
readonly feishuOpenId: string;
|
||||
interface SessionTimes {
|
||||
readonly iat: number;
|
||||
readonly exp: number;
|
||||
}
|
||||
|
||||
export type SessionIdentity =
|
||||
| {
|
||||
readonly userId: string;
|
||||
readonly feishuOpenId: string;
|
||||
}
|
||||
| {
|
||||
readonly userId: string;
|
||||
readonly feishuIdentityId: string;
|
||||
readonly feishuConnectionId: string;
|
||||
readonly feishuOrganizationId: string;
|
||||
};
|
||||
|
||||
export type SessionPayload = SessionIdentity & SessionTimes;
|
||||
|
||||
export interface OAuthStatePayload {
|
||||
readonly nonce: string;
|
||||
readonly returnTo: string;
|
||||
readonly organizationId?: string | undefined;
|
||||
readonly connectionId?: string | undefined;
|
||||
readonly exp: number;
|
||||
}
|
||||
|
||||
export function signSession(
|
||||
payload: Omit<SessionPayload, "iat" | "exp">,
|
||||
payload: SessionIdentity,
|
||||
secret: string,
|
||||
ttlSeconds: number = DEFAULT_SESSION_TTL_SECONDS,
|
||||
nowSeconds: number = Math.floor(Date.now() / 1000),
|
||||
): string {
|
||||
const full: SessionPayload = {
|
||||
userId: payload.userId,
|
||||
feishuOpenId: payload.feishuOpenId,
|
||||
...payload,
|
||||
iat: nowSeconds,
|
||||
exp: nowSeconds + ttlSeconds,
|
||||
};
|
||||
@@ -48,15 +61,38 @@ export function verifySession(
|
||||
secret: string,
|
||||
nowSeconds: number = Math.floor(Date.now() / 1000),
|
||||
): SessionPayload | null {
|
||||
const payload = verifyJson<SessionPayload>(token, secret);
|
||||
const payload = verifyJson<Record<string, unknown>>(token, secret);
|
||||
if (payload === null) return null;
|
||||
if (typeof payload.userId !== "string" || payload.userId === "") return null;
|
||||
if (typeof payload.feishuOpenId !== "string" || payload.feishuOpenId === "") return null;
|
||||
if (typeof payload.iat !== "number" || typeof payload.exp !== "number") return null;
|
||||
if (payload.exp <= nowSeconds) return null;
|
||||
return payload;
|
||||
if (typeof payload["userId"] !== "string" || payload["userId"] === "") return null;
|
||||
if (typeof payload["iat"] !== "number" || typeof payload["exp"] !== "number") return null;
|
||||
if (payload["exp"] <= nowSeconds) return null;
|
||||
const legacy = typeof payload["feishuOpenId"] === "string" && payload["feishuOpenId"] !== "";
|
||||
const scoped = typeof payload["feishuIdentityId"] === "string" && payload["feishuIdentityId"] !== "" &&
|
||||
typeof payload["feishuConnectionId"] === "string" && payload["feishuConnectionId"] !== "" &&
|
||||
typeof payload["feishuOrganizationId"] === "string" && payload["feishuOrganizationId"] !== "";
|
||||
if (legacy === scoped) return null;
|
||||
if (legacy) {
|
||||
return {
|
||||
userId: payload["userId"],
|
||||
feishuOpenId: payload["feishuOpenId"] as string,
|
||||
iat: payload["iat"],
|
||||
exp: payload["exp"],
|
||||
};
|
||||
}
|
||||
return {
|
||||
userId: payload["userId"],
|
||||
feishuIdentityId: payload["feishuIdentityId"] as string,
|
||||
feishuConnectionId: payload["feishuConnectionId"] as string,
|
||||
feishuOrganizationId: payload["feishuOrganizationId"] as string,
|
||||
iat: payload["iat"],
|
||||
exp: payload["exp"],
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* OAuth state may be legacy/unscoped or bind both an Organization and its
|
||||
* intended Feishu connection. A half-scoped state is always invalid.
|
||||
*/
|
||||
export function signOAuthState(
|
||||
payload: Omit<OAuthStatePayload, "exp">,
|
||||
secret: string,
|
||||
@@ -64,8 +100,7 @@ export function signOAuthState(
|
||||
nowSeconds: number = Math.floor(Date.now() / 1000),
|
||||
): string {
|
||||
const full: OAuthStatePayload = {
|
||||
nonce: payload.nonce,
|
||||
returnTo: payload.returnTo,
|
||||
...payload,
|
||||
exp: nowSeconds + ttlSeconds,
|
||||
};
|
||||
return signJson(full, secret);
|
||||
@@ -81,7 +116,17 @@ export function verifyOAuthState(
|
||||
if (typeof payload.nonce !== "string" || payload.nonce === "") return null;
|
||||
if (typeof payload.returnTo !== "string") return null;
|
||||
if (typeof payload.exp !== "number" || payload.exp <= nowSeconds) return null;
|
||||
return payload;
|
||||
const hasOrganization = typeof payload.organizationId === "string" && payload.organizationId !== "";
|
||||
const hasConnection = typeof payload.connectionId === "string" && payload.connectionId !== "";
|
||||
if (hasOrganization !== hasConnection) return null;
|
||||
return {
|
||||
nonce: payload.nonce,
|
||||
returnTo: payload.returnTo,
|
||||
...(hasOrganization
|
||||
? { organizationId: payload.organizationId!, connectionId: payload.connectionId! }
|
||||
: {}),
|
||||
exp: payload.exp,
|
||||
};
|
||||
}
|
||||
|
||||
function signJson(value: unknown, secret: string): string {
|
||||
|
||||
Reference in New Issue
Block a user