feat: add deployable alpha silo

This commit is contained in:
2026-07-11 00:25:45 +08:00
parent 44557da499
commit 9e954790dc
57 changed files with 2792 additions and 400 deletions
+60 -15
View File
@@ -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 {