thaipass 0.1.4 → 0.1.6

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/dist/index.d.ts CHANGED
@@ -24,7 +24,10 @@ interface UpstreamLogger {
24
24
  /** The known ids complete in an editor; any other string is left to the catalog. */
25
25
  export type AipassModelId = KnownChatModel | (string & Record<never, never>);
26
26
  export interface AipassProviderSettings {
27
- /** The Cookie header of a browser signed in to AI Pass. */
27
+ /**
28
+ * The Cookie header of a browser signed in to AI Pass, or a thaipass token
29
+ * when this process holds the `THAIPASS_TOKEN_KEY` that sealed it.
30
+ */
28
31
  readonly cookie: string;
29
32
  /** Process-wide; defaults to https://de.aipass.net. */
30
33
  readonly origin?: string;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { APICallError, NoSuchModelError } from "@ai-sdk/provider";
2
2
  import { z } from "zod";
3
- import { createHash } from "node:crypto";
3
+ import { createDecipheriv, createHash } from "node:crypto";
4
4
  import { parseJsonEventStream } from "@ai-sdk/provider-utils";
5
5
  //#region ../core/src/aipass/models.ts
6
6
  /**
@@ -86,6 +86,185 @@ const configure = (changes) => {
86
86
  Object.assign(current, changes);
87
87
  };
88
88
  //#endregion
89
+ //#region ../core/src/auth/seal.ts
90
+ /**
91
+ * Login with thaipass, without a database.
92
+ *
93
+ * An app that wants to talk to this gateway has to prove it may drive someone's
94
+ * AI Pass account. The obvious way is to hand it the session cookie, which is
95
+ * what every client does today: the key in a Codex config or a Claude Code
96
+ * environment variable is the whole account, it cannot be revoked, and it has
97
+ * to be copied again by hand each time AI Pass rotates it.
98
+ *
99
+ * A thaipass token is that cookie sealed under a key only the gateway holds.
100
+ * The app carries the sealed bytes and can read nothing in them; the gateway
101
+ * unseals per request and forwards the cookie upstream as before. Nothing is
102
+ * stored anywhere, so a deployment stays as stateless as it is now, and the
103
+ * promise the README makes — that the gateway keeps no credential — still
104
+ * holds: what it keeps is the key that opens one, not the credential.
105
+ *
106
+ * The same envelope carries the authorization code in the middle of a login,
107
+ * with the PKCE challenge and the app's redirect inside it, so a code cannot be
108
+ * replayed as an access token or spent by another app.
109
+ */
110
+ const ALGORITHM = "aes-256-gcm";
111
+ const IV_BYTES = 12;
112
+ const TAG_BYTES = 16;
113
+ const KEY_BYTES = 32;
114
+ const TOKEN_PREFIX = "tp_v1_";
115
+ const CODE_PREFIX = "tp_c1_";
116
+ /** The wire shape, with short names because the envelope travels in a header. */
117
+ const payloadSchema = z.object({
118
+ a: z.string().optional(),
119
+ c: z.string(),
120
+ h: z.string().optional(),
121
+ r: z.string().optional(),
122
+ s: z.string().optional(),
123
+ sc: z.string().optional(),
124
+ x: z.number()
125
+ });
126
+ const SECOND_MS = 1e3;
127
+ /** Bytes that authenticated under our own key are still parsed, not trusted. */
128
+ const openPayload = (plain) => {
129
+ try {
130
+ return payloadSchema.parse(JSON.parse(plain));
131
+ } catch {
132
+ return null;
133
+ }
134
+ };
135
+ const NO_KEY$1 = "this deployment issues no thaipass tokens: set THAIPASS_TOKEN_KEY to a 32 byte base64 key";
136
+ const MALFORMED = "the token is not a thaipass token";
137
+ const TAMPERED = "the token does not open: it is for another deployment, or it was altered";
138
+ const EXPIRED = "the token has expired, sign in again";
139
+ const prefixFor = (purpose) => purpose === "code" ? CODE_PREFIX : TOKEN_PREFIX;
140
+ /**
141
+ * The key as bytes, or null when this deployment was never given one. A
142
+ * missing key is not an error: a personal gateway that only ever sees raw
143
+ * cookies needs none, and every path here falls back to that.
144
+ */
145
+ const tokenKey = () => {
146
+ const raw = process.env.THAIPASS_TOKEN_KEY?.trim();
147
+ if (!raw) return null;
148
+ const key = Buffer.from(raw, "base64");
149
+ return key.length === KEY_BYTES ? key : null;
150
+ };
151
+ const issuesTokens = () => tokenKey() !== null;
152
+ const isSealed = (value, purpose = "access") => value.startsWith(prefixFor(purpose));
153
+ const unseal = (value, purpose = "access") => {
154
+ const prefix = prefixFor(purpose);
155
+ if (!value.startsWith(prefix)) return {
156
+ ok: false,
157
+ reason: MALFORMED
158
+ };
159
+ const key = tokenKey();
160
+ if (!key) return {
161
+ ok: false,
162
+ reason: NO_KEY$1
163
+ };
164
+ const sealed = Buffer.from(value.slice(prefix.length), "base64url");
165
+ if (sealed.length <= 28) return {
166
+ ok: false,
167
+ reason: MALFORMED
168
+ };
169
+ const iv = sealed.subarray(0, IV_BYTES);
170
+ const body = sealed.subarray(IV_BYTES, sealed.length - TAG_BYTES);
171
+ const tag = sealed.subarray(sealed.length - TAG_BYTES);
172
+ let plain;
173
+ try {
174
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
175
+ decipher.setAAD(Buffer.from(purpose, "utf-8"));
176
+ decipher.setAuthTag(tag);
177
+ plain = decipher.update(body, void 0, "utf-8") + decipher.final("utf-8");
178
+ } catch {
179
+ return {
180
+ ok: false,
181
+ reason: TAMPERED
182
+ };
183
+ }
184
+ const payload = openPayload(plain);
185
+ if (payload === null) return {
186
+ ok: false,
187
+ reason: TAMPERED
188
+ };
189
+ if (payload.x * SECOND_MS <= Date.now()) return {
190
+ ok: false,
191
+ reason: EXPIRED
192
+ };
193
+ return {
194
+ claims: {
195
+ clientId: payload.a,
196
+ codeChallenge: payload.h,
197
+ cookie: payload.c,
198
+ expiresAt: payload.x,
199
+ redirectUri: payload.r,
200
+ scope: payload.sc,
201
+ subject: payload.s
202
+ },
203
+ ok: true
204
+ };
205
+ };
206
+ //#endregion
207
+ //#region ../core/src/aipass/session.ts
208
+ const SESSION_TOKEN = "__Secure-ai_passport_auth.session_token";
209
+ const NO_TOKEN = `the credential carries no ${SESSION_TOKEN}, send the whole Cookie header from a logged-in browser session rather than the token on its own`;
210
+ const withToken = (cookie, grant) => {
211
+ if (!cookie.includes(SESSION_TOKEN)) return {
212
+ ok: false,
213
+ reason: NO_TOKEN
214
+ };
215
+ return grant ? {
216
+ cookie,
217
+ grant,
218
+ ok: true
219
+ } : {
220
+ cookie,
221
+ ok: true
222
+ };
223
+ };
224
+ /**
225
+ * Two credentials reach the same account. A thaipass token is the cookie
226
+ * sealed under this deployment's key, which is what `thaipass login` hands an
227
+ * app; a raw Cookie header is what a personal setup has always sent. Unsealing
228
+ * happens here, once, so every route accepts either without knowing about it.
229
+ */
230
+ const resolve = (value) => {
231
+ const credential = value.trim();
232
+ if (!isSealed(credential)) return withToken(credential);
233
+ const opened = unseal(credential);
234
+ if (!opened.ok) return {
235
+ ok: false,
236
+ reason: opened.reason
237
+ };
238
+ const { claims } = opened;
239
+ return withToken(claims.cookie, {
240
+ clientId: claims.clientId,
241
+ expiresAt: claims.expiresAt,
242
+ scope: claims.scope,
243
+ subject: claims.subject
244
+ });
245
+ };
246
+ /** A credential handed over directly, as the CLI reads it from the environment or a file. */
247
+ const cookieFromValue = (value) => resolve(value);
248
+ const CLIENT_ID_LENGTH = 16;
249
+ const clientIdFromCookie = (cookie) => createHash("sha256").update(cookie).digest("hex").slice(0, CLIENT_ID_LENGTH);
250
+ //#endregion
251
+ //#region src/credential.ts
252
+ /**
253
+ * What `createAipass` will actually send upstream.
254
+ *
255
+ * `thaipass login` hands people a `tp_…` token now, so one will be pasted in
256
+ * here sooner or later. The provider is not a gateway — it talks to AI Pass
257
+ * directly — so a token is only usable in a process that also holds the key
258
+ * that sealed it. When it does, the token opens; when it does not, saying so
259
+ * beats letting AI Pass refuse the request for reasons of its own.
260
+ */
261
+ const NO_KEY = "that is a thaipass token, and this provider talks to AI Pass directly. Give it the Cookie header, or set THAIPASS_TOKEN_KEY to the key that sealed the token.";
262
+ const resolveCredential = (value) => {
263
+ const lookup = cookieFromValue(value);
264
+ if (lookup.ok) return lookup.cookie;
265
+ throw new Error(isSealed(value) && !issuesTokens() ? NO_KEY : lookup.reason);
266
+ };
267
+ //#endregion
89
268
  //#region ../core/src/lib/cache.ts
90
269
  const ttlCache = (ttlMs) => {
91
270
  const entries = /* @__PURE__ */ new Map();
@@ -199,8 +378,7 @@ const fetchCatalog = async (clientId, cookie, signal) => {
199
378
  * The AI Pass edge answers some requests itself with a 403 before the chat
200
379
  * backend runs, scoring what the prompt contains rather than its length. A
201
380
  * retry of the same body gets the same verdict, so the proxy reports it as a
202
- * request error and hands back the edge's own body, since the trigger set is
203
- * undocumented.
381
+ * request error and hands back the edge's own body.
204
382
  */
205
383
  const EDGE_REFUSAL_STATUS = 403;
206
384
  const EDGE_REFUSAL_HINT = "; the AI Pass edge refused this before the model ran, on what the prompt contains rather than how long it is — resending the same text will be refused again";
@@ -542,8 +720,6 @@ const settleCredits = async (cookie, before) => {
542
720
  usage: toCreditUsage(start, after)
543
721
  };
544
722
  };
545
- const CLIENT_ID_LENGTH = 16;
546
- const clientIdFromCookie = (cookie) => createHash("sha256").update(cookie).digest("hex").slice(0, CLIENT_ID_LENGTH);
547
723
  const thinkingLevelSchema = z.enum([
548
724
  "low",
549
725
  "medium",
@@ -1474,6 +1650,7 @@ const aipassModel = (cookie, modelId) => ({
1474
1650
  //#endregion
1475
1651
  //#region src/index.ts
1476
1652
  const createAipass = (settings) => {
1653
+ const cookie = resolveCredential(settings.cookie);
1477
1654
  if (settings.origin !== void 0) configure({ origin: settings.origin });
1478
1655
  if (settings.logger !== void 0) configure({ logger: settings.logger });
1479
1656
  const languageModel = (modelId = DEFAULT_MODEL) => {
@@ -1481,7 +1658,7 @@ const createAipass = (settings) => {
1481
1658
  modelId,
1482
1659
  modelType: "languageModel"
1483
1660
  });
1484
- return aipassModel(settings.cookie, modelId);
1661
+ return aipassModel(cookie, modelId);
1485
1662
  };
1486
1663
  return Object.assign(languageModel, { languageModel });
1487
1664
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thaipass",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "AI SDK provider for AI Pass, using your own session cookie.",
5
5
  "keywords": [
6
6
  "ai-sdk",