@wenathlan/getry 2.0.164 → 2.0.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth.ts ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @file auth.ts
3
+ * @module gateway/auth
4
+ * @description
5
+ * The gateway request auth and key resolution family of the ownership
6
+ * wave (wave/ownership.gateway): moved verbatim from devthink/server.ts
7
+ * section 1D (the grand-merge interned gateway request auth), so the
8
+ * gateway owns its auth surface — one owner, the family consumes, and
9
+ * devthink re-exports the public face for the compat consumers.
10
+ *
11
+ * Responsibilities isolated here:
12
+ * - Gateway client key resolution — db / env / inline / ephemeral
13
+ * sources walked in the configured order (resolvekeys and the
14
+ * private keysfrom* helpers, keys labeled and length-filtered)
15
+ * - Token extraction / validation / masking from client requests
16
+ * (extracttoken, extractchatid, extractsessionid, extractrequestid,
17
+ * validatetoken, masktoken)
18
+ * - Auth header and query param construction for upstream provider
19
+ * calls (buildauthheaders, buildauthqueryparams, appendqueryparams —
20
+ * the 12 auth methods plus the ride-along headers and params)
21
+ * - Round robin key helpers kept for backward compat (getkey,
22
+ * getkeycount)
23
+ *
24
+ * Architectural constraints:
25
+ * - Verbatim move: every export name and signature preserved — the
26
+ * devthink server keeps answering the family through one re-export
27
+ * shim, so no consumer edits with the move
28
+ * - The db seam stays dynamic: keysfromdb imports the os storage layer
29
+ * through the published "@wenathlan/devthink/storage" specifier (the
30
+ * prisma persistence owner) at call time, so this leaf never links
31
+ * the persistence stack statically and a source-less consumer still
32
+ * loads
33
+ * - Zero external dependencies: global fetch / Buffer beside the
34
+ * owner's types and utils faces
35
+ *
36
+ * Version: 2.1.7 — the ownership wave auth family extraction
37
+ *
38
+ * Related modules:
39
+ * - types.ts — authconfig, resolvedkey (the configuration surface)
40
+ * - utils.ts — genid (the generated identifier family)
41
+ * - engine.ts — the universal request pipeline riding these builders
42
+ * - devthink/server.ts — the compat re-export face of the moved family
43
+ *
44
+ * @author devthink
45
+ * @license MIT
46
+ */
47
+
48
+ import type { authconfig, resolvedkey } from "@wenathlan/getry/types";
49
+ import { genid } from "@wenathlan/getry/utils";
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // inbound extractors — read credentials from client requests
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** extract token — get user supplied token from headers or body */
56
+ export function extracttoken(req: Request, body?: Record<string, unknown>): string {
57
+ return (
58
+ req.headers.get("x-token") ||
59
+ req.headers.get("authorization")?.replace(/^bearer\s+/i, "") ||
60
+ req.headers.get("x-api-key") ||
61
+ req.headers.get("x-goog-api-key") ||
62
+ req.headers.get("api-key") ||
63
+ (body?.token as string) ||
64
+ (body?.apiKey as string) ||
65
+ ""
66
+ );
67
+ }
68
+
69
+ /** extract chat id — get or generate chat id for zai api */
70
+ export function extractchatid(req: Request, body?: Record<string, unknown>): string {
71
+ return (
72
+ req.headers.get("x-chat-id") ||
73
+ req.headers.get("x-user-id") ||
74
+ (body?.chat_id as string) ||
75
+ (body?.chatId as string) ||
76
+ (body?.userId as string) ||
77
+ genid("chat")
78
+ );
79
+ }
80
+
81
+ /** extract session id — get or generate session id for context tracking */
82
+ export function extractsessionid(req: Request, body?: Record<string, unknown>): string {
83
+ return (
84
+ req.headers.get("x-session-id") || (body?.session_id as string) || (body?.sessionId as string) || genid("sess")
85
+ );
86
+ }
87
+
88
+ /** extract request id — get or generate request id */
89
+ export function extractrequestid(req: Request): string {
90
+ return req.headers.get("x-request-id") || genid("req");
91
+ }
92
+
93
+ /** validate token — check if token format is valid */
94
+ export function validatetoken(token: string): boolean {
95
+ if (!token || token.length < 10) return false;
96
+ return true;
97
+ }
98
+
99
+ /** mask token — mask token for logging showing only prefix and suffix */
100
+ export function masktoken(token: string): string {
101
+ if (!token || token.length < 12) return "invalid";
102
+ return `${token.slice(0, 8)}...${token.slice(-4)}`;
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // universal key resolution — db env inline ephemeral
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /** resolve keys from the db source — prisma model lookup */
110
+ async function keysfromdb(auth: authconfig): Promise<resolvedkey[]> {
111
+ const model = auth.dbmodel ?? "apiKey";
112
+ const take = auth.dbtake ?? 50;
113
+ try {
114
+ const { db } = await import("@wenathlan/devthink/storage");
115
+ const table = (
116
+ db as unknown as Record<
117
+ string,
118
+ {
119
+ findMany: (args: Record<string, unknown>) => Promise<Array<Record<string, unknown>>>;
120
+ }
121
+ >
122
+ )[model];
123
+ if (!table) return [];
124
+ const rows = await table.findMany({ where: { active: true, status: "active" }, take });
125
+ return rows
126
+ .map((row, idx) => ({
127
+ key: String(row.keyValue ?? row.key ?? ""),
128
+ label: String(row.label ?? (auth.keylabelformat ?? "key-{idx}").replace("{idx}", String(idx))),
129
+ source: "db" as const,
130
+ }))
131
+ .filter((k) => k.key.length > 0);
132
+ } catch {
133
+ return [];
134
+ }
135
+ }
136
+
137
+ /** resolve keys from the env source — single or separator-joined */
138
+ function keysfromenv(auth: authconfig): resolvedkey[] {
139
+ if (!auth.envvar) return [];
140
+ const raw = process.env[auth.envvar] ?? "";
141
+ const sep = auth.envseparator ?? ",";
142
+ const keys = raw
143
+ .split(sep)
144
+ .map((k) => k.trim())
145
+ .filter(Boolean);
146
+ return keys.map((key, idx) => ({
147
+ key,
148
+ label: (auth.keylabelformat ?? "env-key-{idx}").replace("{idx}", String(idx)),
149
+ source: "env" as const,
150
+ }));
151
+ }
152
+
153
+ /** resolve keys from the inline source — config provided */
154
+ function keysfrominline(auth: authconfig): resolvedkey[] {
155
+ return (auth.inlinekeys ?? []).map((key, idx) => ({
156
+ key,
157
+ label: (auth.keylabelformat ?? "inline-key-{idx}").replace("{idx}", String(idx)),
158
+ source: "inline" as const,
159
+ }));
160
+ }
161
+
162
+ /** resolve keys from the ephemeral url source — babel town pattern */
163
+ async function keysfromephemeral(auth: authconfig): Promise<resolvedkey[]> {
164
+ if (!auth.keyurl) return [];
165
+ try {
166
+ const res = await fetch(auth.keyurl, { method: "GET" });
167
+ const data = (await res.json()) as Record<string, unknown>;
168
+ const key = String(data.key ?? data.api_key ?? data.token ?? "");
169
+ if (!key) return [];
170
+ return [{ key, label: "ephemeral", source: "ephemeralurl" as const }];
171
+ } catch {
172
+ return [];
173
+ }
174
+ }
175
+
176
+ /** resolvekeys — resolve all available keys per the auth config source order */
177
+ export async function resolvekeys(auth: authconfig): Promise<resolvedkey[]> {
178
+ const sources = auth.keysources ?? ["env"];
179
+ const minlen = auth.minkeylength ?? 10;
180
+ const all: resolvedkey[] = [];
181
+ for (const source of sources) {
182
+ if (source === "db") all.push(...(await keysfromdb(auth)));
183
+ else if (source === "env") all.push(...keysfromenv(auth));
184
+ else if (source === "inline") all.push(...keysfrominline(auth));
185
+ else if (source === "ephemeralurl") all.push(...(await keysfromephemeral(auth)));
186
+ }
187
+ return all.filter((k) => k.key.length >= minlen);
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // outbound header building — per auth method
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /** buildauthheaders — build upstream auth headers from the auth config and key */
195
+ export function buildauthheaders(auth: authconfig, key: string): Record<string, string> {
196
+ const headers: Record<string, string> = {};
197
+ switch (auth.mode) {
198
+ case "bearer":
199
+ case "oauth2clientcredentials":
200
+ case "jwtsign":
201
+ headers["authorization"] = `Bearer ${key}`;
202
+ break;
203
+ case "apikeyheader":
204
+ headers[auth.headername ?? "x-api-key"] = key;
205
+ break;
206
+ case "basic": {
207
+ const encoded = Buffer.from(key).toString("base64");
208
+ headers["authorization"] = `Basic ${encoded}`;
209
+ break;
210
+ }
211
+ case "queryparam":
212
+ case "cookie":
213
+ case "mtls":
214
+ case "sigv4":
215
+ case "hmacsign":
216
+ case "keylesssdk":
217
+ case "anonymous":
218
+ case "none":
219
+ // these methods do not use simple headers — handled by transport or url
220
+ break;
221
+ }
222
+ // ride-along headers — anthropic-version azure api-version http-referer x-title etc
223
+ for (const [k, v] of Object.entries(auth.extraheaders ?? {})) {
224
+ headers[k] = v;
225
+ }
226
+ return headers;
227
+ }
228
+
229
+ /** buildauthqueryparams — build query params for queryparam auth mode */
230
+ export function buildauthqueryparams(auth: authconfig, key: string): Record<string, string> {
231
+ const params: Record<string, string> = {};
232
+ if (auth.mode === "queryparam") {
233
+ params[auth.queryparamname ?? "key"] = key;
234
+ }
235
+ for (const [k, v] of Object.entries(auth.extraparams ?? {})) {
236
+ params[k] = v;
237
+ }
238
+ return params;
239
+ }
240
+
241
+ /** appendqueryparams — append query params to a url string */
242
+ export function appendqueryparams(url: string, params: Record<string, string>): string {
243
+ const entries = Object.entries(params);
244
+ if (entries.length === 0) return url;
245
+ const separator = url.includes("?") ? "&" : "?";
246
+ const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
247
+ return `${url}${separator}${qs}`;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // legacy helpers — kept for backward compat with existing callers
252
+ // ---------------------------------------------------------------------------
253
+
254
+ /** get key by index — simple round robin helper for single key lookups */
255
+ export async function getkey(auth: authconfig, index = 0): Promise<string> {
256
+ const keys = await resolvekeys(auth);
257
+ if (keys.length === 0) return "";
258
+ return keys[index % keys.length].key;
259
+ }
260
+
261
+ /** get key count — total resolvable keys */
262
+ export async function getkeycount(auth: authconfig): Promise<number> {
263
+ const keys = await resolvekeys(auth);
264
+ return keys.length;
265
+ }