Compare commits

..

No commits in common. "34e9c4a76dfdcc2546db67588116aac12a91ac83" and "f5c44f8ac5de188e40a1eb03b68b853e734d4145" have entirely different histories.

15 changed files with 3 additions and 1054 deletions

View File

@ -2,18 +2,16 @@
"workspace": [ "workspace": [
"./sdk", "./sdk",
"./server", "./server",
"./ui", "./ui"
"./src"
], ],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"tasks": { "tasks": {
"dev": "deno run --watch -A --unstable-ffi server/main.ts", "dev": "deno run --watch -A --unstable-ffi server/main.ts",
"start": "deno run -A --unstable-ffi server/main.ts", "start": "deno run -A --unstable-ffi server/main.ts",
"test": "deno test -A --unstable-ffi", "test": "deno test -A --unstable-ffi",
"lint": "deno lint && deno run -A scripts/lint_arch.ts", "lint": "deno lint",
"lint:arch": "deno run -A scripts/lint_arch.ts",
"fmt": "deno fmt", "fmt": "deno fmt",
"check": "deno check server/**/*.ts sdk/**/*.ts ui/**/*.ts infra/**/*.ts src/**/*.ts src/**/*.tsx", "check": "deno check server/**/*.ts sdk/**/*.ts ui/**/*.ts infra/**/*.ts",
"setup": "deno run -A infra/setup.ts", "setup": "deno run -A infra/setup.ts",
"release": "deno run -A infra/setup.ts release" "release": "deno run -A infra/setup.ts release"
}, },

22
deno.lock generated
View File

@ -20,14 +20,10 @@
"jsr:@std/encoding@~1.0.5": "1.0.10", "jsr:@std/encoding@~1.0.5": "1.0.10",
"jsr:@std/fmt@0.225.2": "0.225.2", "jsr:@std/fmt@0.225.2": "0.225.2",
"jsr:@std/fmt@~1.0.2": "1.0.8", "jsr:@std/fmt@~1.0.2": "1.0.8",
"jsr:@std/fs@*": "1.0.24",
"jsr:@std/internal@1": "1.0.14", "jsr:@std/internal@1": "1.0.14",
"jsr:@std/internal@^1.0.12": "1.0.14", "jsr:@std/internal@^1.0.12": "1.0.14",
"jsr:@std/internal@^1.0.14": "1.0.14",
"jsr:@std/io@~0.224.9": "0.224.9", "jsr:@std/io@~0.224.9": "0.224.9",
"jsr:@std/path@*": "1.0.9",
"jsr:@std/path@0.225.2": "0.225.2", "jsr:@std/path@0.225.2": "0.225.2",
"jsr:@std/path@^1.1.5": "1.1.6",
"jsr:@std/path@~1.0.6": "1.0.9", "jsr:@std/path@~1.0.6": "1.0.9",
"jsr:@std/testing@*": "1.0.20", "jsr:@std/testing@*": "1.0.20",
"jsr:@std/text@~1.0.7": "1.0.19", "jsr:@std/text@~1.0.7": "1.0.19",
@ -136,13 +132,6 @@
"@std/fmt@1.0.8": { "@std/fmt@1.0.8": {
"integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
}, },
"@std/fs@1.0.24": {
"integrity": "f3061b45b81673a2bece689da041df32d174be064c89eb6397fb5718d3fb7877",
"dependencies": [
"jsr:@std/internal@^1.0.14",
"jsr:@std/path@^1.1.5"
]
},
"@std/internal@1.0.14": { "@std/internal@1.0.14": {
"integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
}, },
@ -158,12 +147,6 @@
"@std/path@1.0.9": { "@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
}, },
"@std/path@1.1.6": {
"integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe",
"dependencies": [
"jsr:@std/internal@^1.0.14"
]
},
"@std/testing@1.0.20": { "@std/testing@1.0.20": {
"integrity": "21380ed438672762e4ec549cbf4fe41c5b68f5598773a30b64abe7375513e721", "integrity": "21380ed438672762e4ec549cbf4fe41c5b68f5598773a30b64abe7375513e721",
"dependencies": [ "dependencies": [
@ -529,11 +512,6 @@
"npm:@connectrpc/connect@^1.4.0" "npm:@connectrpc/connect@^1.4.0"
], ],
"members": { "members": {
"src": {
"dependencies": [
"jsr:@hono/hono@4"
]
},
"ui": { "ui": {
"dependencies": [ "dependencies": [
"jsr:@hono/hono@4" "jsr:@hono/hono@4"

View File

@ -1,55 +0,0 @@
import { walk } from "jsr:@std/fs";
const targetDir = "./src";
let hasErrors = false;
async function checkFile(path: string) {
const content = await Deno.readTextFile(path);
const lines = content.split("\n");
lines.forEach((line, index) => {
// 1. Block banned DOM APIs
if (
line.includes("document.getElementById") ||
line.includes("document.querySelector") ||
line.includes("document.createElement")
) {
console.error(
`[Arch Lint] ❌ Banned DOM API used in ${path}:${index + 1}`,
);
console.error(` ${line.trim()}`);
console.error(
` -> Use Datastar reactive attributes or SSE morphs instead.`,
);
hasErrors = true;
}
// 2. Block unescaped HTML in raw strings (basic heuristic for dangerouslySetInnerHTML)
if (
line.includes("dangerouslySetInnerHTML") &&
!path.includes("error_fragments.tsx")
) {
console.error(
`[Arch Lint] ❌ dangerouslySetInnerHTML used in ${path}:${index + 1}`,
);
console.error(` ${line.trim()}`);
console.error(
` -> Native JSX HTML escaping should be used unless in explicit core fragments.`,
);
hasErrors = true;
}
});
}
for await (const entry of walk(targetDir, { exts: [".ts", ".tsx"] })) {
if (entry.isFile) {
await checkFile(entry.path);
}
}
if (hasErrors) {
Deno.exit(1);
} else {
console.log("✅ Architecture lint passed.");
}

View File

@ -1,16 +0,0 @@
import { assertEquals } from "jsr:@std/assert";
import { determineClientType } from "./content_negotiation.ts";
import type { Context } from "jsr:@hono/hono@4";
Deno.test("determineClientType - correctly identifies datastar", () => {
const mockContext = {
req: {
header: (name: string) => {
if (name === "datastar-request") return "true";
return "";
},
},
} as unknown as Context;
assertEquals(determineClientType(mockContext), "datastar");
});

View File

@ -1,102 +0,0 @@
import type { Context, Next } from "jsr:@hono/hono@4";
import { HTTPException } from "jsr:@hono/hono@4/http-exception";
import { valkey } from "./valkey.ts";
const MAX_BODY_SIZE = 16 * 1024; // 16KB
export async function payloadCapGuard(c: Context, next: Next) {
const contentLength = c.req.header("content-length");
if (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {
throw new HTTPException(413, {
message: "Payload Too Large: Max 16KB allowed.",
});
}
// Also guard if they stream/chunk it without content-length but we can't easily do that natively in middleware without consuming body
await next();
}
export function csrfOriginGuard(allowedOrigins: string[]) {
return async (c: Context, next: Next) => {
// Only check mutating methods
if (["POST", "PUT", "PATCH", "DELETE"].includes(c.req.method)) {
const origin = c.req.header("origin");
const referer = c.req.header("referer");
const host = c.req.header("host");
// Simple check: if origin matches one of allowed origins or if origin matches host
if (origin) {
let isAllowed = false;
try {
const originUrl = new URL(origin);
if (
allowedOrigins.includes(originUrl.origin) || originUrl.host === host
) {
isAllowed = true;
}
} catch (_e) {
// invalid url
}
if (!isAllowed) {
throw new HTTPException(403, {
message: "Forbidden: Invalid Origin for CSRF protection.",
});
}
} else if (referer) {
try {
const refererUrl = new URL(referer);
if (
allowedOrigins.includes(refererUrl.origin) ||
refererUrl.host === host
) {
// allowed
} else {
throw new HTTPException(403, {
message: "Forbidden: Invalid Referer for CSRF protection.",
});
}
} catch (_e) {
throw new HTTPException(403, {
message: "Forbidden: Invalid Referer for CSRF protection.",
});
}
} else {
// Enforce presence of Origin or Referer for mutating requests (basic CSRF protection)
throw new HTTPException(403, {
message:
"Forbidden: Origin or Referer header required for CSRF protection.",
});
}
}
await next();
};
}
export function rateLimitGuard(
limit: number,
windowSecs: number,
keyPrefix: string,
) {
return async (c: Context, next: Next) => {
// Simple sliding/fixed window rate limit implementation using Valkey
const ip = c.req.header("x-forwarded-for")?.split(",")[0] || "unknown-ip";
const key = `ratelimit:${keyPrefix}:${ip}`;
try {
const current = await valkey.incr(key);
if (current === 1) {
await valkey.expire(key, windowSecs);
}
if (current > limit) {
throw new HTTPException(429, { message: "Too Many Requests" });
}
} catch (err) {
if (err instanceof HTTPException) throw err;
// If Valkey is down, we might want to log it and allow, or fail open
console.warn("[RateLimit] Valkey error:", err);
}
await next();
};
}

View File

@ -1,39 +0,0 @@
import type { Context, Next } from "jsr:@hono/hono@4";
export type ClientType = "datastar" | "cli" | "shell" | "browser";
export function determineClientType(c: Context): ClientType {
const accept = c.req.header("accept") || "";
const userAgent = c.req.header("user-agent") || "";
if (c.req.header("datastar-request") === "true") {
return "datastar";
}
if (
accept.includes("application/json") || userAgent.includes("curl") ||
userAgent.includes("AuthYesCLI")
) {
// If it's explicitly asking for JSON, or it's a CLI tool like curl/our custom CLI
if (accept.includes("application/json")) {
return "cli";
}
// curl without explicit accept json might just want text/plain shell strings
return "shell";
}
if (accept.includes("text/html")) {
return "browser";
}
// Default to cli/json if unknown
return "cli";
}
export function contentNegotiation() {
return async (c: Context, next: Next) => {
const clientType = determineClientType(c);
c.set("clientType", clientType);
await next();
};
}

View File

@ -1,364 +0,0 @@
import postgres from "npm:postgres@3";
// Evaluate environment variables dynamically to prevent crash during test imports
const host = Deno.env.get("POSTGRES_HOST") ||
(import.meta.main ? "" : "localhost");
const user = Deno.env.get("POSTGRES_USER") ||
(import.meta.main ? "" : "postgres");
const password = Deno.env.get("POSTGRES_PASSWORD") ||
(import.meta.main ? "" : "postgres");
const db = Deno.env.get("POSTGRES_DB") || (import.meta.main ? "" : "postgres");
const port = Deno.env.get("POSTGRES_PORT") || "5432";
if (import.meta.main && (!host || !user || !password || !db)) {
throw new Error(
"Missing critical database environment variables. Required: POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB.",
);
}
const connectionString = `postgres://${user}:${password}@${host}:${port}/${db}`;
// Export it as let so we can override it in tests
export let sql = postgres(connectionString);
/**
* SIDE EFFECT: Initializes the database schema.
*/
export async function initDb(): Promise<void> {
console.log("[Auth DB] Initializing central identity database schema...");
// We ensure new users are 'pending' to satisfy Use Case 3 (Manual state machine activation)
// If the table exists we will attempt to alter the default.
await sql`
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE NOT NULL,
display_name TEXT,
account_status TEXT DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
try {
await sql`ALTER TABLE users ALTER COLUMN account_status SET DEFAULT 'pending'`;
await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()`;
} catch {
// Ignore if unsupported
}
await sql`
CREATE TABLE IF NOT EXISTS apps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
spiffe_id VARCHAR(255) UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
// Ensure app_secret column exists for API validation mapping
try {
await sql`ALTER TABLE apps ADD COLUMN IF NOT EXISTS app_secret TEXT UNIQUE`;
} catch {
// Soft ignore if column already exists
// Ignore and check next
}
// Ensure domain column exists for Edge authorization routing
try {
await sql`ALTER TABLE apps ADD COLUMN IF NOT EXISTS domain TEXT UNIQUE`;
} catch {
// Soft ignore if column already exists
}
// Tier 1 Ingress Control
try {
await sql`ALTER TABLE apps ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT FALSE`;
await sql`ALTER TABLE apps ADD COLUMN IF NOT EXISTS bypass_paths TEXT[] DEFAULT '{}'`;
await sql`ALTER TABLE apps ADD COLUMN IF NOT EXISTS allowed_cidrs TEXT[] DEFAULT '{}'`;
} catch {
// Soft ignore if columns already exist
}
await sql`
CREATE TABLE IF NOT EXISTS roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
app_id UUID REFERENCES apps(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(name, app_id)
);
`;
// Seed standard global roles if table is empty
try {
const existingRoles = await sql`SELECT count(*)::int as count FROM roles`
.then((res) => res[0]?.count || 0);
if (existingRoles === 0) {
await sql`
INSERT INTO roles (name, description, app_id) VALUES
('admin', 'Full administrative access across all management capabilities', NULL),
('editor', 'Read and write access with permissions to modify records', NULL),
('operator', 'Operational execution access for runtime tasks', NULL),
('viewer', 'Read-only access across application telemetry and views', NULL)
ON CONFLICT DO NOTHING
`;
}
} catch {
// Ignore seed errors on race conditions
}
await sql`
CREATE TABLE IF NOT EXISTS grants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'user',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, app_id)
);
`;
await sql`
CREATE TABLE IF NOT EXISTS invites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
app_id UUID REFERENCES apps(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'user',
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
max_uses INT DEFAULT 1,
uses_count INT DEFAULT 0,
auto_activate BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used_at TIMESTAMP WITH TIME ZONE,
used_by UUID REFERENCES users(id) ON DELETE SET NULL
);
`;
// Migrations for existing invites table
try {
await sql`ALTER TABLE invites ADD COLUMN IF NOT EXISTS max_uses INT DEFAULT 1`;
await sql`ALTER TABLE invites ADD COLUMN IF NOT EXISTS uses_count INT DEFAULT 0`;
await sql`ALTER TABLE invites ADD COLUMN IF NOT EXISTS auto_activate BOOLEAN DEFAULT TRUE`;
} catch {
// Ignore migration column exists
}
await sql`
CREATE TABLE IF NOT EXISTS invite_redemptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invite_id UUID NOT NULL REFERENCES invites(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
redeemed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
await sql`
CREATE TABLE IF NOT EXISTS aaguid_allowlist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aaguid UUID UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
await sql`
CREATE TABLE IF NOT EXISTS recovery_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used_at TIMESTAMP WITH TIME ZONE
);
`;
await sql`
CREATE TABLE IF NOT EXISTS recovery_shares (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
server_share TEXT NOT NULL,
pin_hash TEXT NOT NULL,
attempts_count INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
await sql`
CREATE TABLE IF NOT EXISTS audit_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL,
resource TEXT,
details JSONB,
ip_address TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
leaf_hash TEXT
);
`;
try {
await sql`ALTER TABLE audit_records ADD COLUMN IF NOT EXISTS leaf_hash TEXT`;
} catch {
// Ignore migration column exists
}
await sql`
CREATE TABLE IF NOT EXISTS event_passes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
pin_code TEXT UNIQUE,
name TEXT NOT NULL,
app_id UUID REFERENCES apps(id) ON DELETE SET NULL,
role TEXT DEFAULT 'viewer',
max_seats INT DEFAULT 50,
seats_claimed INT DEFAULT 0,
lifespan_hours INT DEFAULT 3,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
is_active BOOLEAN DEFAULT TRUE,
is_paused BOOLEAN DEFAULT FALSE,
expires_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
try {
await sql`ALTER TABLE event_passes ADD COLUMN IF NOT EXISTS is_paused BOOLEAN DEFAULT FALSE`;
await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS event_pass_id UUID REFERENCES event_passes(id) ON DELETE CASCADE`;
// Backfill legacy event passes to ensure older guests can still be revoked/extended
await sql`
UPDATE users u
SET event_pass_id = ep.id
FROM event_passes ep
WHERE u.account_status = 'guest'
AND u.event_pass_id IS NULL
AND u.username LIKE 'guest_' || ep.slug || '\\_%'
`;
} catch {
// Ignore migration column exists
}
await sql`
CREATE TABLE IF NOT EXISTS audit_sths (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tree_size BIGINT NOT NULL,
root_hash TEXT NOT NULL,
signature TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
await sql`
CREATE TABLE IF NOT EXISTS passkeys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
credential_id TEXT UNIQUE NOT NULL,
public_key TEXT NOT NULL,
counter BIGINT NOT NULL,
prf_enabled BOOLEAN DEFAULT FALSE,
prf_salt TEXT
);
`;
// Ensure prf columns exist
try {
await sql`ALTER TABLE passkeys ADD COLUMN IF NOT EXISTS prf_enabled BOOLEAN DEFAULT FALSE`;
await sql`ALTER TABLE passkeys ADD COLUMN IF NOT EXISTS prf_salt TEXT`;
} catch {
// Ignore migration column exists
}
await sql`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label TEXT,
is_agent BOOLEAN DEFAULT FALSE,
custom_scopes TEXT[],
last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_activity_action TEXT,
is_paused BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
);
`;
try {
await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS label TEXT`;
await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_agent BOOLEAN DEFAULT FALSE`;
await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS custom_scopes TEXT[]`;
await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()`;
await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS last_activity_action TEXT`;
await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_paused BOOLEAN DEFAULT FALSE`;
} catch {
// Ignore migration column exists
}
await sql`
CREATE TABLE IF NOT EXISTS hwk_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fingerprint TEXT UNIQUE NOT NULL,
public_key JSONB NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`;
// Seed ed-droid app record with spiffe_id
await sql`
INSERT INTO apps (name, spiffe_id)
VALUES ('ed-droid', 'spiffe://system.local/ed-droid-backend')
ON CONFLICT (spiffe_id) DO NOTHING
`;
// Seed the Central Auth-Yes Management App for global administration
await sql`
INSERT INTO apps (name, spiffe_id)
VALUES ('Auth-Yes Management Console', 'spiffe://system.local/auth-yes-management')
ON CONFLICT (spiffe_id) DO NOTHING
`;
// Seed initial bootstrap admin invite if no users exist in the database
try {
const userCount = await sql`SELECT count(*)::int as count FROM users`.then(
(res) => res[0]?.count || 0,
);
if (userCount === 0) {
const inviteCount = await sql`
SELECT count(*)::int as count FROM invites
WHERE role = 'admin' AND expires_at > NOW()
`.then((res) => res[0]?.count || 0);
if (inviteCount === 0) {
const bootstrapCode = Deno.env.get("BOOTSTRAP_INVITE_CODE") ||
"bootstrap-admin";
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
await sql`
INSERT INTO invites (code, app_id, role, max_uses, uses_count, auto_activate, expires_at)
VALUES (${bootstrapCode}, NULL, 'admin', 1, 0, TRUE, ${expiresAt})
ON CONFLICT (code) DO NOTHING
`;
console.log(
`[Auth DB] Initial bootstrap admin invite code seeded: '${bootstrapCode}'`,
);
}
}
} catch (err) {
console.warn("[Auth DB] Bootstrap invite check skipped:", err);
}
console.log("[Auth DB] Central identity database schema initialized.");
}
export const sqlWrapper = {
get sql() {
return sql;
},
set sql(val: any) {
sql = val;
},
};

View File

@ -1,43 +0,0 @@
import { html } from "jsr:@hono/hono@4/html";
export function renderErrorToastFragment(message: string, isError = true) {
const bgClass = isError ? "bg-red-500" : "bg-green-500";
const icon = isError ? "⚠️" : "✅";
// Renders a toast fragment intended to morph onto a #status-banner or similar container
return html`
<div id="status-banner" data-signals="{show: true}"
class="fixed top-4 right-4 z-50 transition-all duration-300">
<div
class="flex items-center gap-2 ${bgClass} text-white px-4 py-3 rounded shadow-lg"
role="alert" aria-live="polite">
<span>${icon}</span>
<span class="font-medium">${message}</span>
<button data-on-click="$show = false" aria-label="Close"
class="ml-4 hover:opacity-80">
</button>
</div>
<style>
#status-banner { display: block; opacity: 1; transform: translateY(0); }
#status-banner[data-signals*='"show":false'],
#status-banner[data-signals*='"show": false'] {
opacity: 0;
transform: translateY(-20px);
pointer-events: none;
}
</style>
</div>
`;
}
export function renderFieldErrorFragment(fieldId: string, errorMsg: string) {
// Merges the field error specifically under the input field
// Assumes a convention like <p id="{fieldId}-error" class="text-red-500 text-sm mt-1"></p>
return html`
<p id="${fieldId}-error" class="text-red-500 text-sm mt-1" role="alert"
aria-live="polite">
${errorMsg}
</p>
`;
}

View File

@ -1,263 +0,0 @@
import { X509Certificate } from "npm:@peculiar/x509";
import { AsnParser } from "npm:@peculiar/asn1-schema";
import { SubjectAlternativeName } from "npm:@peculiar/asn1-x509";
// Deno binding for the spire_ffi Rust crate
if (Deno.build.arch !== "x86_64" && Deno.build.arch !== "aarch64") {
throw new Error("Unsupported architecture");
}
const libPath = (() => {
if (Deno.build.os === "windows") {
return "../spire_ffi/target/release/spire_ffi.dll";
}
if (Deno.build.os === "darwin") {
return "../spire_ffi/target/release/libspire_ffi.dylib";
}
return "../spire_ffi/target/release/libspire_ffi.so";
})();
let dylib: Deno.DynamicLibrary<any> | null = null;
try {
dylib = Deno.dlopen(libPath, {
fetch_svid: {
parameters: ["pointer"],
result: "pointer",
nonblocking: true,
},
free_svid: {
parameters: ["pointer"],
result: "void",
},
argon2id_derive: {
parameters: [
"pointer",
"usize",
"pointer",
"usize",
"u32",
"u32",
"pointer",
"usize",
],
result: "i32",
nonblocking: true,
},
});
} catch (_e) {
console.warn(
`Failed to load ${libPath}. Workload API fetching will be mocked/disabled if used.`,
);
}
export interface SvidResponse {
spiffe_id: string;
x509_svid: Uint8Array;
x509_svid_key: Uint8Array;
bundle: Uint8Array;
}
export async function fetchSpiffeIdentity(
socketPath: string = "/var/run/spire/agent.sock",
): Promise<SvidResponse> {
if (!dylib) {
console.warn(
`[SPIRE FFI] Dynamic library (${libPath}) is not loaded. Mocking SVID response for local development.`,
);
return {
spiffe_id: "spiffe://local.dev/mock",
x509_svid: new Uint8Array(),
x509_svid_key: new Uint8Array(),
bundle: new Uint8Array(),
};
}
const encoder = new TextEncoder();
const encodedPath = encoder.encode(socketPath + "\0");
const pathPtr = Deno.UnsafePointer.of(encodedPath);
const fetch_svid = dylib.symbols
.fetch_svid as unknown as ((
ptr: Deno.PointerValue,
) => Promise<Deno.PointerValue>);
const free_svid = dylib.symbols
.free_svid as unknown as ((ptr: Deno.PointerValue) => void);
const resPtr = await fetch_svid(pathPtr);
if (resPtr === null) {
throw new Error("fetch_svid returned a null pointer");
}
const resView = new Deno.UnsafePointerView(resPtr);
let errorMsg: string | null = null;
let spiffe_id: string | null = null;
let offset = 0;
const ptrSize = 8; // 64-bit pointers
const spiffe_id_ptr = resView.getPointer(offset);
offset += ptrSize;
const x509_svid_ptr = resView.getPointer(offset);
offset += ptrSize;
const x509_svid_len = Number(resView.getBigUint64(offset));
offset += ptrSize;
const x509_svid_key_ptr = resView.getPointer(offset);
offset += ptrSize;
const x509_svid_key_len = Number(resView.getBigUint64(offset));
offset += ptrSize;
const bundle_ptr = resView.getPointer(offset);
offset += ptrSize;
const bundle_len = Number(resView.getBigUint64(offset));
offset += ptrSize;
const error_ptr = resView.getPointer(offset);
if (error_ptr !== null) {
errorMsg = new Deno.UnsafePointerView(error_ptr).getCString();
}
if (errorMsg !== null) {
free_svid(resPtr);
throw new Error(errorMsg);
}
if (spiffe_id_ptr !== null) {
spiffe_id = new Deno.UnsafePointerView(spiffe_id_ptr).getCString();
}
if (!spiffe_id) {
free_svid(resPtr);
throw new Error("spiffe_id is null");
}
const x509_svid = x509_svid_ptr !== null && x509_svid_len > 0
? new Uint8Array(
new Deno.UnsafePointerView(x509_svid_ptr).getArrayBuffer(x509_svid_len),
)
: new Uint8Array();
const x509_svid_key = x509_svid_key_ptr !== null && x509_svid_key_len > 0
? new Uint8Array(
new Deno.UnsafePointerView(x509_svid_key_ptr).getArrayBuffer(
x509_svid_key_len,
),
)
: new Uint8Array();
const bundle = bundle_ptr !== null && bundle_len > 0
? new Uint8Array(
new Deno.UnsafePointerView(bundle_ptr).getArrayBuffer(bundle_len),
)
: new Uint8Array();
// Create copies of the typed arrays before freeing the memory
const svidData = {
spiffe_id,
x509_svid: new Uint8Array(x509_svid),
x509_svid_key: new Uint8Array(x509_svid_key),
bundle: new Uint8Array(bundle),
};
// Free the memory on the Rust side
free_svid(resPtr);
return svidData;
}
/**
* Extracts the SPIFFE ID from an incoming client TLS connection.
*/
export let extractSpiffeIdFromCert = function extractSpiffeIdFromCert(
certBundle: string,
): string | null {
if (!certBundle || typeof certBundle !== "string") {
return null;
}
try {
const cert = new X509Certificate(certBundle);
const sanExtension = cert.extensions.find((ext) =>
ext.type === "2.5.29.17"
); // Subject Alternative Name
if (!sanExtension) {
return null;
}
const san = AsnParser.parse(sanExtension.value, SubjectAlternativeName);
for (const name of san) {
if (
name.uniformResourceIdentifier &&
name.uniformResourceIdentifier.startsWith("spiffe://")
) {
return name.uniformResourceIdentifier;
}
}
} catch (_e) {
return null;
}
return null;
};
export async function deriveArgon2idKey(
password: Uint8Array,
salt: Uint8Array,
): Promise<Uint8Array> {
const outBuf = new Uint8Array(32);
if (!dylib) {
console.warn(
`[SPIRE FFI] Dynamic library (${libPath}) is not loaded. Mocking Argon2id derivation for local development.`,
);
outBuf.fill(0xaa);
return outBuf;
}
const iterations = 12;
const memoryKb = 65536;
const passwordPtr = Deno.UnsafePointer.of(password);
const saltPtr = Deno.UnsafePointer.of(salt);
const outPtr = Deno.UnsafePointer.of(outBuf);
const argon2id_derive = dylib.symbols
.argon2id_derive as unknown as ((
passwordPtr: Deno.PointerValue,
passwordLen: number | bigint,
saltPtr: Deno.PointerValue,
saltLen: number | bigint,
iterations: number,
memoryKb: number,
outPtr: Deno.PointerValue,
outLen: number | bigint,
) => Promise<number>);
const res = await argon2id_derive(
passwordPtr,
password.length,
saltPtr,
salt.length,
iterations,
memoryKb,
outPtr,
outBuf.length,
);
if (res !== 0) {
throw new Error(`Argon2id derivation failed: ${res}`);
}
return outBuf;
}
export const spireWrapper = {
get extractSpiffeIdFromCert() {
return extractSpiffeIdFromCert;
},
set extractSpiffeIdFromCert(val: any) {
extractSpiffeIdFromCert = val;
},
};

View File

@ -1,44 +0,0 @@
import type { Context } from "jsr:@hono/hono@4";
import { streamSSE } from "jsr:@hono/hono@4/streaming";
export interface DatastarSSEEvent {
event?: string; // usually 'datastar-fragment', 'datastar-signal', etc
data: string; // The HTML fragment or JSON payload
id?: string;
retry?: number;
}
export function streamDatastar(
c: Context,
callback: (stream: {
write: (event: DatastarSSEEvent) => Promise<void>;
close: () => Promise<void>;
sleep: (ms: number) => Promise<void>;
aborted: boolean;
}) => Promise<void>,
) {
return streamSSE(c, async (stream) => {
// Add custom datastar helper methods
const adapter = {
write: async (event: DatastarSSEEvent) => {
await stream.writeSSE({
data: event.data,
event: event.event || "datastar-fragment",
id: event.id,
retry: event.retry,
});
},
close: async () => {
await stream.close();
},
sleep: async (ms: number) => {
await stream.sleep(ms);
},
get aborted() {
return stream.aborted;
},
};
await callback(adapter);
});
}

View File

@ -1,42 +0,0 @@
import { Redis } from "npm:ioredis";
const VALKEY_URL = Deno.env.get("VALKEY_URL") ||
(import.meta.main ? "redis://auth-valkey:6379" : "");
export const valkey = VALKEY_URL
? new Redis(VALKEY_URL, {
enableOfflineQueue: false,
maxRetriesPerRequest: 1,
retryStrategy: (times) => (times > 3 ? null : Math.min(times * 100, 1000)),
})
: new Redis({
lazyConnect: true,
enableOfflineQueue: false,
maxRetriesPerRequest: 1,
retryStrategy: () => null,
});
valkey.on("error", (err) => {
if (Deno.env.get("DEBUG_VALKEY")) {
console.warn("[Valkey] Connection warning:", err.message);
}
});
export async function pingValkey(): Promise<void> {
if (!VALKEY_URL) return; // Skip in test
try {
const result = await valkey.ping();
if (result !== "PONG") {
throw new Error(`Unexpected ping response: ${result}`);
}
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Fatal: Failed to connect to Valkey session cache. Halting boot. ${error.message}`,
);
}
throw new Error(
`Fatal: Failed to connect to Valkey session cache. Halting boot.`,
);
}
}

View File

@ -1,9 +0,0 @@
{
"name": "@auth-yes/src",
"version": "0.1.0",
"exports": "./main.ts",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "jsr:@hono/hono@4/jsx"
}
}

View File

@ -1,23 +0,0 @@
import { Hono } from "jsr:@hono/hono@4";
import { serveStatic } from "jsr:@hono/hono@4/deno";
import { initDb } from "./core/db.ts";
import { pingValkey } from "./core/valkey.ts";
const app: Hono = new Hono();
// Serve static assets (specifically Datastar)
app.use("/public/*", serveStatic({ root: "./" }));
// Basic health check for foundation
app.get("/healthz", (c) => c.text("OK"));
if (import.meta.main) {
console.log("[Auth-Yes Next] Bootstrapping core foundation...");
await initDb();
await pingValkey();
const port = parseInt(Deno.env.get("PORT") || "8000", 10);
Deno.serve({ port }, app.fetch);
}
export default app;

View File

@ -1,27 +0,0 @@
# Post-Implementation Audit: Phase 1 (Hypermedia Architecture)
## 1. Test Suite Status
- **`deno fmt`**: Passed (After 2 minor corrections in test files and scripts)
- **`deno task lint`**: Passed (Fixed one verbatim-module-syntax warning in test imports)
- **`deno task check`**: Passed
- **`deno test -A --no-check`**: Passed (68 tests across 30 steps)
## 2. Structural & Architectural Verification
- **Directory Placement:** `src/core/` successfully established with all requested single-purpose modules (`db.ts`, `valkey.ts`, `spire_ffi.ts`, `auth_guards.ts`, `content_negotiation.ts`, `sse_adapter.ts`, `error_fragments.tsx`, `main.ts`).
- **Legacy Quarantine:** Jules did NOT modify any files in the read-only `server/` or `ui/` directories.
- **Dependency Invariants:** `sdk/` and `spire_ffi/` were kept isolated.
- **Architectural Linting Hook:** `scripts/lint_arch.ts` successfully wired. It correctly rejects banned DOM APIs (`getElementById`, `querySelector`, `createElement`) and `dangerouslySetInnerHTML`.
## 3. Code Hygiene & Rule Alignment
- **Flat Call Chains:** Confirmed. Functions in `src/core/auth_guards.ts` (e.g., `rateLimitGuard`, `payloadCapGuard`) are shallow and self-contained.
- **Datastar SSE Typing:** `sse_adapter.ts` correctly isolates raw protocol strings from route handlers.
- **Error Handling:** `error_fragments.tsx` conforms to the invariant to return target-directed HTML toasts instead of JSON errors.
## 4. Issues Addressed
1. Jules forgot to run a final `deno fmt` on two newly created files. (Fixed)
2. Jules included an import type issue caught by the standard linter. (Fixed via `deno lint --fix`)
3. Jules eagerly moved the Task Plan to `tasks/complete/`. Because this is a 5-phase migration, I moved it to `tasks/in-progress/` to maintain continuity.
## 5. Go / No-Go Decision
**Decision: GO.** 🟢
Phase 1 is strictly verified, cleanly isolated in `src/`, and the repository quality gates are 100% green. We are cleared to commence Phase 2.