im_wower
·
2026-03-21
model.ts
1import type { AuthAction } from "./actions.js";
2
3export const AUTH_ROLES = [
4 "controller",
5 "worker",
6 "browser_admin",
7 "ops_admin",
8 "readonly"
9] as const;
10
11export type AuthRole = (typeof AUTH_ROLES)[number];
12
13export const AUTH_TOKEN_KINDS = [
14 "service_hmac",
15 "service_signed",
16 "browser_session",
17 "ops_session"
18] as const;
19
20export type AuthTokenKind = (typeof AUTH_TOKEN_KINDS)[number];
21
22export const DEFAULT_AUTH_AUDIENCE = "control_api";
23
24export type AuthAudience = typeof DEFAULT_AUTH_AUDIENCE;
25export type AuthIdentityBinding = "machine" | "session" | "ops";
26
27export interface AuthPrincipal {
28 subject: string;
29 role: AuthRole;
30 tokenKind: AuthTokenKind;
31 audience: AuthAudience;
32 tokenId?: string;
33 issuer?: string;
34 issuedAt?: string;
35 expiresAt?: string;
36 controllerId?: string;
37 workerId?: string;
38 nodeId?: string;
39 sessionId?: string;
40 scope?: AuthAction[];
41}
42
43export interface AuthResourceOwnership {
44 controllerId?: string;
45 workerId?: string;
46}
47
48export interface AuthTokenModel {
49 kind: AuthTokenKind;
50 summary: string;
51 intendedRoles: readonly AuthRole[];
52 status: "current" | "future";
53 credentialShape: "shared_secret" | "signed_claims" | "bearer_secret";
54 identityBinding: AuthIdentityBinding;
55}
56
57export const AUTH_TOKEN_MODELS = {
58 service_hmac: {
59 kind: "service_hmac",
60 summary: "当前最低要求。controller 与 worker 使用共享密钥或 HMAC 签名材料。",
61 intendedRoles: ["controller", "worker"],
62 status: "current",
63 credentialShape: "shared_secret",
64 identityBinding: "machine"
65 },
66 service_signed: {
67 kind: "service_signed",
68 summary: "后续演进。把 role、scope、节点身份放进可验证的签名 service token。",
69 intendedRoles: ["controller", "worker"],
70 status: "future",
71 credentialShape: "signed_claims",
72 identityBinding: "machine"
73 },
74 browser_session: {
75 kind: "browser_session",
76 summary: "可见 control 浏览器会话使用的 bearer token,用于 browser_admin 与 readonly。",
77 intendedRoles: ["browser_admin", "readonly"],
78 status: "current",
79 credentialShape: "bearer_secret",
80 identityBinding: "session"
81 },
82 ops_session: {
83 kind: "ops_session",
84 summary: "运维人员专用 bearer token,权限与浏览器会话分离。",
85 intendedRoles: ["ops_admin"],
86 status: "current",
87 credentialShape: "bearer_secret",
88 identityBinding: "ops"
89 }
90} satisfies Record<AuthTokenKind, AuthTokenModel>;
91
92export type BearerTokenFailureReason =
93 | "missing_authorization_header"
94 | "invalid_authorization_scheme"
95 | "empty_bearer_token";
96
97export type BearerTokenExtractionResult =
98 | {
99 ok: true;
100 token: string;
101 }
102 | {
103 ok: false;
104 reason: BearerTokenFailureReason;
105 };
106
107export type AuthVerificationFailureReason =
108 | "unknown_token"
109 | "invalid_signature"
110 | "token_expired"
111 | "audience_mismatch";
112
113export type AuthVerificationResult =
114 | {
115 ok: true;
116 principal: AuthPrincipal;
117 }
118 | {
119 ok: false;
120 reason: AuthVerificationFailureReason;
121 statusCode: 401 | 403;
122 };
123
124export interface AuthTokenVerifier {
125 verifyBearerToken(token: string): Promise<AuthVerificationResult>;
126}
127
128export function extractBearerToken(
129 authorizationHeader: string | undefined
130): BearerTokenExtractionResult {
131 if (!authorizationHeader) {
132 return {
133 ok: false,
134 reason: "missing_authorization_header"
135 };
136 }
137
138 const [scheme, ...rest] = authorizationHeader.trim().split(/\s+/u);
139
140 if (scheme !== "Bearer") {
141 return {
142 ok: false,
143 reason: "invalid_authorization_scheme"
144 };
145 }
146
147 const token = rest.join(" ").trim();
148
149 if (!token) {
150 return {
151 ok: false,
152 reason: "empty_bearer_token"
153 };
154 }
155
156 return {
157 ok: true,
158 token
159 };
160}