blob: 36c62af545df2688a9e27549cd5e98dbe8bc1eb1 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { z } from "zod"
import { BUILD_CONTEXT_MARKER, defineTool, endpointArg, error, parseEndpoint, text } from "../lib.ts"
import actionAddRedis from "./add-redis.ts"
interface FileSnapshot {
path: string
existed: boolean
content: string
}
function snapshot(path: string): FileSnapshot {
return {
path,
existed: existsSync(path),
content: existsSync(path) ? readFileSync(path, "utf-8") : "",
}
}
function restore(files: FileSnapshot[]): void {
for (const file of files) {
if (file.existed) writeFileSync(file.path, file.content)
else rmSync(file.path, { force: true })
}
}
export default defineTool({
name: "auth_setup",
config: {
description: "Atomically add Redis session wiring to the complete authentication surface: token issuers (login/registration), token validators (me/session and every protected resource), and logout. This tool never reads or writes .env. Generated auth must use bcrypt password verification plus a cryptographically random opaque token stored as a prefix-safe ctx.REDIS_PREFIX key with a bounded TTL; protected endpoints derive identity from Redis and logout deletes the record. JWT and application signing secrets are forbidden.",
inputSchema: {
token_endpoints: z.array(endpointArg).min(1).describe("All endpoints that issue an authenticated session, such as v1/login and v1/register"),
protected_endpoints: z.array(endpointArg).min(1).describe("The me/session endpoint and every endpoint that requires an authenticated identity"),
logout_endpoints: z.array(endpointArg).min(1).describe("All endpoints that revoke an authenticated session, normally v1/logout"),
},
},
async handler({ token_endpoints, protected_endpoints, logout_endpoints }) {
const endpoints = [...new Set([...token_endpoints, ...protected_endpoints, ...logout_endpoints])]
const parsed: ReturnType<typeof parseEndpoint>[] = []
try {
// WHY: validate the complete graph before writing anything. A missing
// protected endpoint must not leave only login wired, which would make
// the application appear authenticated while its backend is unusable.
for (const endpoint of endpoints) {
const action = parseEndpoint(endpoint)
if (!existsSync(action.mainPath)) {
return error(`Authentication setup failed: endpoint not found at ${action.mainPath}. No endpoint was changed.`)
}
const wrapper = readFileSync(action.mainPath, "utf-8")
if (!wrapper.includes(BUILD_CONTEXT_MARKER)) {
return error(`Authentication setup failed: marker '${BUILD_CONTEXT_MARKER}' not found in ${action.mainPath}. No endpoint was changed.`)
}
parsed.push(action)
}
const snapshots = parsed.flatMap((action) => [
snapshot(action.mainPath),
snapshot(join(action.dir, "requirements.txt")),
])
try {
for (const endpoint of endpoints) {
const result = await actionAddRedis.handler({ endpoint })
if (result.isError) {
restore(snapshots)
const detail = result.content.map((part) => part.text).join("\n")
return error(`Authentication setup failed while wiring '${endpoint}': ${detail}. All endpoint files were restored.`)
}
}
} catch (cause) {
restore(snapshots)
throw cause
}
return text([
`Redis authentication wiring is ready for ${endpoints.length} endpoints: ${endpoints.join(", ")}.`,
"Token endpoints must verify password hashes with bcrypt, generate a cryptographically random opaque token, and store only a token-to-identity record in Redis with a bounded TTL.",
"Build keys without duplicate separators: append an app-local 'session:<token>' suffix to ctx.REDIS_PREFIX, adding ':' only when the configured prefix does not already end with it.",
"Protected endpoints must read the Bearer token, load the Redis record, derive user id and roles only from that record, and fail closed when it is absent or expired.",
"Logout endpoints must delete the same Redis record. The browser stores only the opaque token.",
"Do not use JWT, an application signing secret, browser-supplied user ids, or direct edits to generated __main__.py wrappers.",
].join("\n"))
} catch (cause) {
return error(`Authentication setup failed: ${(cause as Error).message}`)
}
},
})