sf snapshot: uncommitted changes after 268m inactivity
This commit is contained in:
parent
7e1631618a
commit
def1edefa9
21 changed files with 306 additions and 23 deletions
|
|
@ -1,2 +1,2 @@
|
|||
[tools]
|
||||
node = "26.1.0"
|
||||
node = "26"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"useDefineForClassFields": false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"useDefineForClassFields": false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"useDefineForClassFields": false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"types": ["node"],
|
||||
|
|
|
|||
66
packages/google-gemini-cli-provider/src/index.d.ts
vendored
Normal file
66
packages/google-gemini-cli-provider/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { type ContentGenerator } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js";
|
||||
export interface GeminiCliContentGeneratorOptions {
|
||||
modelId: string;
|
||||
cwd?: string;
|
||||
targetDir?: string;
|
||||
}
|
||||
/**
|
||||
* Create a Gemini CLI Core content generator for a model.
|
||||
*
|
||||
* Purpose: centralize the Code Assist setup and OAuth bootstrap logic in a
|
||||
* reusable package so SF's Gemini provider can stay focused on stream shaping.
|
||||
*
|
||||
* Consumer: the Google Gemini provider in pi-ai.
|
||||
*/
|
||||
export declare function createGeminiCliContentGenerator(options: GeminiCliContentGeneratorOptions): Promise<ContentGenerator>;
|
||||
/**
|
||||
* Per-model quota bucket from CodeAssistServer.retrieveUserQuota.
|
||||
*/
|
||||
export interface GeminiQuotaBucket {
|
||||
modelId: string;
|
||||
usedFraction: number;
|
||||
remainingFraction: number;
|
||||
resetTime?: string;
|
||||
}
|
||||
/**
|
||||
* Snapshot of the active gemini-cli account: tier identity, project, and the
|
||||
* full per-model quota table.
|
||||
*
|
||||
* Why a single struct: every consumer (model picker, usage UI, capacity
|
||||
* diagnostics, catalog cache) needs the same three pieces of data. Returning
|
||||
* them together avoids three separate OAuth round trips.
|
||||
*/
|
||||
export interface GeminiAccountSnapshot {
|
||||
projectId: string;
|
||||
/** Active tier id from setupUser.userTier (e.g. "free-tier", "standard-tier"). */
|
||||
userTierId?: string;
|
||||
/** Active tier human label from setupUser.userTierName. */
|
||||
userTierName?: string;
|
||||
/**
|
||||
* Paid tier descriptor when the account has one (e.g. AI Ultra). Carries
|
||||
* id like "g1-ultra-tier" and the marketing name. Distinct from the
|
||||
* effective userTier — a free-tier session can still have a paidTier
|
||||
* marker if the underlying account is subscribed.
|
||||
*/
|
||||
paidTier?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
models: GeminiQuotaBucket[];
|
||||
}
|
||||
/**
|
||||
* Discover the active gemini-cli account: tier, project, and every model the
|
||||
* account has access to (with per-model usage fraction and reset time).
|
||||
*
|
||||
* Best-effort: any failure (OAuth expired, no project, network) returns null
|
||||
* silently so callers can downgrade gracefully.
|
||||
*
|
||||
* Consumer: SF-side background catalog cache, usage UI, capacity diagnostics.
|
||||
*/
|
||||
export declare function snapshotGeminiCliAccount(cwd?: string): Promise<GeminiAccountSnapshot | null>;
|
||||
/**
|
||||
* Convenience wrapper: just the model IDs the active gemini-cli account has
|
||||
* access to. Returns null on failure (same contract as snapshotGeminiCliAccount).
|
||||
*/
|
||||
export declare function discoverGeminiCliModels(cwd?: string): Promise<string[] | null>;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
packages/google-gemini-cli-provider/src/index.d.ts.map
Normal file
1
packages/google-gemini-cli-provider/src/index.d.ts.map
Normal file
File diff suppressed because one or more lines are too long
99
packages/google-gemini-cli-provider/src/index.js
Normal file
99
packages/google-gemini-cli-provider/src/index.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Google Gemini CLI transport helper.
|
||||
*
|
||||
* Purpose: keep the Gemini CLI Core auth and content-generator wiring in a
|
||||
* dedicated workspace package so provider code can depend on one small helper
|
||||
* instead of embedding the upstream integration inline.
|
||||
*
|
||||
* Consumer: `@singularity-forge/ai` Google Gemini provider, plus SF-side
|
||||
* background catalog discovery.
|
||||
*/
|
||||
import { AuthType, CodeAssistServer, getOauthClient, makeFakeConfig, setupUser, } from "@google/gemini-cli-core";
|
||||
import { createContentGenerator, createContentGeneratorConfig, } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js";
|
||||
/**
|
||||
* Create a Gemini CLI Core content generator for a model.
|
||||
*
|
||||
* Purpose: centralize the Code Assist setup and OAuth bootstrap logic in a
|
||||
* reusable package so SF's Gemini provider can stay focused on stream shaping.
|
||||
*
|
||||
* Consumer: the Google Gemini provider in pi-ai.
|
||||
*/
|
||||
export async function createGeminiCliContentGenerator(options) {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const config = makeFakeConfig({
|
||||
model: options.modelId,
|
||||
cwd,
|
||||
targetDir: options.targetDir ?? cwd,
|
||||
});
|
||||
const generatorConfig = await createContentGeneratorConfig(config, AuthType.LOGIN_WITH_GOOGLE);
|
||||
return createContentGenerator(generatorConfig, config);
|
||||
}
|
||||
/**
|
||||
* Discover the active gemini-cli account: tier, project, and every model the
|
||||
* account has access to (with per-model usage fraction and reset time).
|
||||
*
|
||||
* Best-effort: any failure (OAuth expired, no project, network) returns null
|
||||
* silently so callers can downgrade gracefully.
|
||||
*
|
||||
* Consumer: SF-side background catalog cache, usage UI, capacity diagnostics.
|
||||
*/
|
||||
export async function snapshotGeminiCliAccount(cwd) {
|
||||
try {
|
||||
const config = makeFakeConfig({ cwd: cwd ?? process.cwd() });
|
||||
const authClient = await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config);
|
||||
const userData = await setupUser(authClient, config);
|
||||
const projectId = userData?.projectId;
|
||||
if (!projectId || typeof projectId !== "string")
|
||||
return null;
|
||||
const server = new CodeAssistServer(authClient, projectId, { headers: {} });
|
||||
const data = await server.retrieveUserQuota({ project: projectId });
|
||||
// Dedup buckets per modelId, keeping the WORST quota (lowest
|
||||
// remainingFraction). Code Assist sometimes returns multiple buckets
|
||||
// for the same model when more than one quota window applies; the
|
||||
// pessimistic choice is what every consumer (UI, capacity diagnostics,
|
||||
// model picker) actually wants to surface.
|
||||
const byModel = new Map();
|
||||
for (const b of data?.buckets ?? []) {
|
||||
const modelId = typeof b.modelId === "string" ? b.modelId : "";
|
||||
if (!modelId)
|
||||
continue;
|
||||
const remainingFraction = typeof b.remainingFraction === "number" ? b.remainingFraction : 1;
|
||||
const bucket = {
|
||||
modelId,
|
||||
usedFraction: 1 - remainingFraction,
|
||||
remainingFraction,
|
||||
resetTime: typeof b.resetTime === "string" ? b.resetTime : undefined,
|
||||
};
|
||||
const existing = byModel.get(modelId);
|
||||
if (!existing || bucket.remainingFraction < existing.remainingFraction) {
|
||||
byModel.set(modelId, bucket);
|
||||
}
|
||||
}
|
||||
const models = Array.from(byModel.values()).sort((a, b) => a.modelId.localeCompare(b.modelId));
|
||||
if (models.length === 0)
|
||||
return null;
|
||||
return {
|
||||
projectId,
|
||||
userTierId: typeof userData?.userTier === "string" ? userData.userTier : undefined,
|
||||
userTierName: userData?.userTierName,
|
||||
paidTier: userData?.paidTier
|
||||
? { id: userData.paidTier.id, name: userData.paidTier.name }
|
||||
: undefined,
|
||||
models,
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Convenience wrapper: just the model IDs the active gemini-cli account has
|
||||
* access to. Returns null on failure (same contract as snapshotGeminiCliAccount).
|
||||
*/
|
||||
export async function discoverGeminiCliModels(cwd) {
|
||||
const snap = await snapshotGeminiCliAccount(cwd);
|
||||
if (!snap)
|
||||
return null;
|
||||
return snap.models.map((m) => m.modelId);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
packages/google-gemini-cli-provider/src/index.js.map
Normal file
1
packages/google-gemini-cli-provider/src/index.js.map
Normal file
File diff suppressed because one or more lines are too long
2
packages/google-gemini-cli-provider/src/index.test.d.ts
vendored
Normal file
2
packages/google-gemini-cli-provider/src/index.test.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export {};
|
||||
//# sourceMappingURL=index.test.d.ts.map
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["index.test.ts"],"names":[],"mappings":"","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, test, vi } from \"vitest\";\n\nconst helperState = vi.hoisted(() => ({\n\tauthType: undefined as unknown,\n\tconfigParams: undefined as Record<string, unknown> | undefined,\n}));\n\nvi.mock(\"@google/gemini-cli-core\", () => ({\n\tAuthType: { LOGIN_WITH_GOOGLE: \"LOGIN_WITH_GOOGLE\" },\n\tmakeFakeConfig: vi.fn((params: Record<string, unknown>) => {\n\t\thelperState.configParams = params;\n\t\treturn { params };\n\t}),\n}));\n\nvi.mock(\"@google/gemini-cli-core/dist/src/core/contentGenerator.js\", () => ({\n\tcreateContentGeneratorConfig: vi.fn(async (_config, authType) => {\n\t\thelperState.authType = authType;\n\t\treturn { authType };\n\t}),\n\tcreateContentGenerator: vi.fn(async () => ({\n\t\tasync generateContentStream(): Promise<AsyncGenerator<unknown>> {\n\t\t\treturn (async function* emptyStream() {})();\n\t\t},\n\t})),\n}));\n\nimport { createGeminiCliContentGenerator } from \"./index.js\";\n\ndescribe(\"google-gemini-cli-provider\", () => {\n\ttest(\"createGeminiCliContentGenerator_uses_google_login_auth\", async () => {\n\t\tawait createGeminiCliContentGenerator({ modelId: \"gemini-3-pro\" });\n\n\t\tassert.equal(helperState.authType, \"LOGIN_WITH_GOOGLE\");\n\t\tassert.equal(helperState.configParams?.model, \"gemini-3-pro\");\n\t\tassert.equal(helperState.configParams?.cwd, process.cwd());\n\t\tassert.equal(helperState.configParams?.targetDir, process.cwd());\n\t});\n});\n"]}
|
||||
35
packages/google-gemini-cli-provider/src/index.test.js
Normal file
35
packages/google-gemini-cli-provider/src/index.test.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, test, vi } from "vitest";
|
||||
const helperState = vi.hoisted(() => ({
|
||||
authType: undefined,
|
||||
configParams: undefined,
|
||||
}));
|
||||
vi.mock("@google/gemini-cli-core", () => ({
|
||||
AuthType: { LOGIN_WITH_GOOGLE: "LOGIN_WITH_GOOGLE" },
|
||||
makeFakeConfig: vi.fn((params) => {
|
||||
helperState.configParams = params;
|
||||
return { params };
|
||||
}),
|
||||
}));
|
||||
vi.mock("@google/gemini-cli-core/dist/src/core/contentGenerator.js", () => ({
|
||||
createContentGeneratorConfig: vi.fn(async (_config, authType) => {
|
||||
helperState.authType = authType;
|
||||
return { authType };
|
||||
}),
|
||||
createContentGenerator: vi.fn(async () => ({
|
||||
async generateContentStream() {
|
||||
return (async function* emptyStream() { })();
|
||||
},
|
||||
})),
|
||||
}));
|
||||
import { createGeminiCliContentGenerator } from "./index.js";
|
||||
describe("google-gemini-cli-provider", () => {
|
||||
test("createGeminiCliContentGenerator_uses_google_login_auth", async () => {
|
||||
await createGeminiCliContentGenerator({ modelId: "gemini-3-pro" });
|
||||
assert.equal(helperState.authType, "LOGIN_WITH_GOOGLE");
|
||||
assert.equal(helperState.configParams?.model, "gemini-3-pro");
|
||||
assert.equal(helperState.configParams?.cwd, process.cwd());
|
||||
assert.equal(helperState.configParams?.targetDir, process.cwd());
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=index.test.js.map
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["index.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACrC,QAAQ,EAAE,SAAoB;IAC9B,YAAY,EAAE,SAAgD;CAC9D,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,QAAQ,EAAE,EAAE,iBAAiB,EAAE,mBAAmB,EAAE;IACpD,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,MAA+B,EAAE,EAAE;QACzD,WAAW,CAAC,YAAY,GAAG,MAAM,CAAC;QAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IACnB,CAAC,CAAC;CACF,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,2DAA2D,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3E,4BAA4B,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC/D,WAAW,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAChC,OAAO,EAAE,QAAQ,EAAE,CAAC;IACrB,CAAC,CAAC;IACF,sBAAsB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAC1C,KAAK,CAAC,qBAAqB;YAC1B,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,WAAW,KAAI,CAAC,CAAC,EAAE,CAAC;QAC7C,CAAC;KACD,CAAC,CAAC;CACH,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAC;AAE7D,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC3C,IAAI,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,+BAA+B,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;QAEnE,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC;QACxD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;QAC9D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, test, vi } from \"vitest\";\n\nconst helperState = vi.hoisted(() => ({\n\tauthType: undefined as unknown,\n\tconfigParams: undefined as Record<string, unknown> | undefined,\n}));\n\nvi.mock(\"@google/gemini-cli-core\", () => ({\n\tAuthType: { LOGIN_WITH_GOOGLE: \"LOGIN_WITH_GOOGLE\" },\n\tmakeFakeConfig: vi.fn((params: Record<string, unknown>) => {\n\t\thelperState.configParams = params;\n\t\treturn { params };\n\t}),\n}));\n\nvi.mock(\"@google/gemini-cli-core/dist/src/core/contentGenerator.js\", () => ({\n\tcreateContentGeneratorConfig: vi.fn(async (_config, authType) => {\n\t\thelperState.authType = authType;\n\t\treturn { authType };\n\t}),\n\tcreateContentGenerator: vi.fn(async () => ({\n\t\tasync generateContentStream(): Promise<AsyncGenerator<unknown>> {\n\t\t\treturn (async function* emptyStream() {})();\n\t\t},\n\t})),\n}));\n\nimport { createGeminiCliContentGenerator } from \"./index.js\";\n\ndescribe(\"google-gemini-cli-provider\", () => {\n\ttest(\"createGeminiCliContentGenerator_uses_google_login_auth\", async () => {\n\t\tawait createGeminiCliContentGenerator({ modelId: \"gemini-3-pro\" });\n\n\t\tassert.equal(helperState.authType, \"LOGIN_WITH_GOOGLE\");\n\t\tassert.equal(helperState.configParams?.model, \"gemini-3-pro\");\n\t\tassert.equal(helperState.configParams?.cwd, process.cwd());\n\t\tassert.equal(helperState.configParams?.targetDir, process.cwd());\n\t});\n});\n"]}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext"}}]}<()>;等待下一步操作。<|assistant to=multi_tool_use.parallel __(/*!json*/)## Step: Rebuild and rerun autonomous SF after tsconfig updates. Also, verify build and run success before marking complete. {
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"useDefineForClassFields": false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"useDefineForClassFields": false,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noEmit": true,
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"types": ["node"],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"module": "Node16",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2024"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": false,
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"useDefineForClassFields": false,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
||||
import { homedir, platform } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
|
|
|||
77
web/middleware.ts
Normal file
77
web/middleware.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
* Next.js middleware — validates bearer token and origin on all API routes.
|
||||
*
|
||||
* The SF_WEB_AUTH_TOKEN env var is set at server launch. Every /api/* request
|
||||
* must carry a matching `Authorization: Bearer <token>` header. EventSource
|
||||
* (SSE) connections may use the `_token` query parameter instead since the
|
||||
* EventSource API cannot set custom headers.
|
||||
*
|
||||
* Additionally, if an `Origin` header is present, it must match the expected
|
||||
* localhost origin to prevent cross-site request forgery.
|
||||
*/
|
||||
export function middleware(request: NextRequest): NextResponse {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Only gate API routes
|
||||
if (!pathname.startsWith("/api/")) return NextResponse.next();
|
||||
|
||||
const expectedToken = process.env.SF_WEB_AUTH_TOKEN;
|
||||
if (!expectedToken) {
|
||||
// If no token was configured (e.g. dev mode without launch harness),
|
||||
// allow everything — the server didn't opt into auth.
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// ── Origin / CORS check ────────────────────────────────────────────
|
||||
const origin = request.headers.get("origin");
|
||||
if (origin) {
|
||||
const host = process.env.SF_WEB_HOST || "127.0.0.1";
|
||||
const port = process.env.SF_WEB_PORT || "3000";
|
||||
|
||||
// Default: localhost origin for the launched host:port
|
||||
const allowed = new Set([`http://${host}:${port}`]);
|
||||
|
||||
// SF_WEB_ALLOWED_ORIGINS lets users whitelist additional origins for
|
||||
// secure tunnel setups (Tailscale Serve, Cloudflare Tunnel, ngrok, etc.)
|
||||
const extra = process.env.SF_WEB_ALLOWED_ORIGINS;
|
||||
if (extra) {
|
||||
for (const entry of extra.split(",")) {
|
||||
const trimmed = entry.trim();
|
||||
if (trimmed) allowed.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowed.has(origin)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden: origin mismatch" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bearer token check ─────────────────────────────────────────────
|
||||
let token: string | null = null;
|
||||
|
||||
// 1. Authorization header (preferred)
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
token = authHeader.slice(7);
|
||||
}
|
||||
|
||||
// 2. Query parameter fallback for EventSource / SSE
|
||||
if (!token) {
|
||||
token = request.nextUrl.searchParams.get("_token");
|
||||
}
|
||||
|
||||
if (!token || token !== expectedToken) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: "/api/:path*",
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue