Compare commits

..

7 Commits

Author SHA1 Message Date
11786de7e4
Merge pull request #6 from mrteye/feat-spire-ffi-native-argon2-task-4427490188496264791
feat(spire-ffi): task plan for native Argon2id derivation
2026-08-23 20:57:41 -07:00
4b72d491bf
Merge pull request #4 from mrteye/feat-sdk-realtime-invalidation-bus-task-2670591133327528715
docs(tasks): draft realtime invalidation bus task for SDK
2026-08-23 20:57:34 -07:00
a3e157608e
Merge pull request #5 from mrteye/feat-ingress-grant-injection-task-592912514149159076
docs: draft task file for Ingress Grant Injection
2026-08-23 20:57:28 -07:00
google-labs-jules[bot]
2b3b6f9636 feat(spire-ffi): create planning task for native Argon2id FFI
- Generated Kanban markdown file adhering to tasks/GUIDELINES.md.
- Documented requirements for SIMD, Deno FFI non-blocking bindings, and `hash-wasm` alternatives.
- Specified development/production fallback strategies.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-24 03:57:17 +00:00
google-labs-jules[bot]
38310f4ead docs: draft task file for Ingress Grant Injection in forward-auth
Creates a new task markdown file in `tasks/new` detailing the
architecture and implementation steps required to inject
flattened RBAC grants via Valkey caching for `/api/forward-auth`.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-24 03:56:04 +00:00
google-labs-jules[bot]
1f94f27b57 docs(tasks): draft realtime invalidation bus task for SDK
Creates a Kanban task file outlining the architectural plan and
implementation steps for adding a zero-dependency real-time event
bus to `@auth-yes/sdk`. This bus will listen for Valkey RESP3 push
invalidation events and emit raw token strings to registered
listeners, ensuring safe and isolated execution to prevent downstream
crashes.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-24 03:55:00 +00:00
google-labs-jules[bot]
2753b6b757 docs(tasks): draft realtime invalidation bus task for SDK
Creates a Kanban task file outlining the architectural plan and
implementation steps for adding a zero-dependency real-time event
bus to `@auth-yes/sdk`. This bus will listen for Valkey RESP3 push
invalidation events and emit raw token strings to registered
listeners, ensuring safe and isolated execution to prevent downstream
crashes.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-24 03:54:55 +00:00
3 changed files with 197 additions and 0 deletions

View File

@ -0,0 +1,46 @@
# TASK METADATA
- **Target Files:** `spire_ffi/Cargo.toml`, `spire_ffi/src/lib.rs`, `server/spire_ffi.ts`, `server/spire_ffi.test.ts`
- **Core Objective:** Implement high-throughput native Argon2id derivation (12 iterations, 64 MiB memory, 128-bit salt, 256-bit output key) via the `spire_ffi` Rust crate to accelerate zero-knowledge hashing.
- **Dependencies:** The Deno application must be able to compile or load the Rust dynamic library (`libspire_ffi.so` / `spire_ffi.dll` / `libspire_ffi.dylib`).
- **Additional Important Notes:** Fallback behavior for local dev/testing must mimic the existing pattern in `spire_ffi.ts`, returning deterministic mock data when the library is unavailable, but throwing a fatal error in production contexts.
---
## Architectural Considerations & Risks
### Risks
- **FFI Memory Safety:** Exposing a new C-compatible function via FFI (`argon2id_derive`) involves raw pointer manipulation. Incorrectly sizing the output buffer, failing to free memory properly, or reading past the bounds of input buffers (password/salt) could lead to segfaults or memory leaks.
- **Platform Compatibility:** Relying on native code means the target environment must support the compiled architecture. The CI/CD pipeline must build the Rust crate for all supported deployment targets (x86_64, aarch64).
- **Blocking the Event Loop:** Hashing with Argon2id is intentionally slow and CPU-intensive. If the Deno FFI call is blocking, it will stall the Deno event loop. The FFI function must be marked as `nonblocking: true` in `Deno.dlopen`, and the Rust implementation must be executed asynchronously or off the main thread.
### Alternatives
- **WASM (e.g., `hash-wasm`):** An alternative is to use WebAssembly to perform the hashing directly in Deno without FFI.
- *Justification against:* Native SIMD provides 3–5x greater throughput compared to V8 WebAssembly for 64 MiB memory-hard hashing. Furthermore, we already have and maintain the `spire_ffi` Rust workspace. Avoiding a new npm dependency keeps the server and `@auth-yes/sdk` zero-dependency, adhering to our core architectural goals.
## Proposed Implementation
### 1. Update Rust Dependencies
- Add the `argon2` crate to `spire_ffi/Cargo.toml`:
```toml
[dependencies]
argon2 = { version = "0.5", features = ["std"] }
```
- *Note on SIMD:* Version 0.5.x automatically uses runtime CPU feature detection (e.g., `std::is_x86_feature_detected!("avx2")`). For portable builds, use standard `cargo build --release`. For optional optimized container builds, document the use of `RUSTFLAGS="-C target-cpu=native" cargo build --release`.
### 2. Implement Rust FFI Export
- In `spire_ffi/src/lib.rs`, create a new `#[no_mangle]` extern "C" function named `argon2id_derive`.
- The function signature should accept pointers and lengths for the password and salt, iteration count, memory size (in KB), and a pointer/length for the output buffer.
- Use the `argon2` crate to compute the hash and write the result into the provided output buffer. Ensure all FFI safety boundaries are respected (e.g., checking for null pointers).
### 3. Expose via Deno FFI (`server/spire_ffi.ts`)
- Add the `argon2id_derive` symbol to the `Deno.dlopen` definition. Critically, set `nonblocking: true` to prevent stalling the Deno event loop during hash computation.
- Create an exported async function `deriveArgon2idKey(password: Uint8Array, salt: Uint8Array): Promise<Uint8Array>`.
- Hardcode the security parameters: 12 iterations, 64 MiB (65536 KB) memory, and a 32-byte (256-bit) output key length.
- Implement the fallback pattern: If `dylib` is null, log `[SPIRE FFI] Dynamic library (...) is not loaded. Mocking Argon2id derivation for local development.` and return a deterministic 32-byte mock buffer (e.g., filled with a repeating pattern).
### 4. Write Unit Tests
- In `server/spire_ffi.test.ts`, write tests to ensure:
- The fallback mechanism works when the library isn't loaded (returns the mock buffer).
- When the library *is* available, the function successfully returns a 32-byte Uint8Array.
- (Optional) Verify the output matches a known standard Argon2id test vector to confirm correctness across the FFI boundary.

View File

@ -0,0 +1,69 @@
# TASK METADATA
- **Target Files:** `sdk/mod.ts`, `sdk/mod.test.ts`
- **Core Objective:** Extend `@auth-yes/sdk` with a zero-dependency real-time event bus to listen for Valkey 8 RESP3 push invalidation events and emit them to registered listeners.
- **Dependencies:** None.
- **Additional Important Notes:** Handlers must support async execution. Errors thrown by handlers must be caught and logged so they do not crash the Valkey push listener or stop other registered handlers. No external EventEmitter libraries are allowed.
---
## Architectural Considerations & Risks
### Risks
- **Memory Leaks:** Downstream consumers registering listeners for `"invalidate"` without subsequently calling `off` could result in an unbounded map of listeners. We must clearly document the `off` method and emphasize its usage during cleanup (e.g., when a consumer class is destroyed or a connection pool closes).
- **Callback Crash Propagation:** If a downstream consumer's callback throws an exception (e.g., failing to terminate a WebSocket), and it is not caught, it could crash the internal Valkey `push` event handler. The implementation must guarantee isolated execution via a `try/catch` wrapper and gracefully handle Promise rejections.
- **Payload Alignment:** Any discrepancy between the raw key emitted by Valkey and the expected token by downstream listeners could result in missed invalidations. As confirmed, Valkey stores and emits the raw `token` without prefixes, so we must emit the token strictly as received.
### Alternatives
- Using the web standard `EventTarget` natively supported in Deno. While native, standard `EventTarget` limits the payload to custom `Event` objects (`CustomEvent`), which adds boilerplate (`event.detail.token`) and type-casting complexity for downstream users. A custom lightweight callback Set (`Set<InvalidationHandler>`) allows us to pass the `token: string` payload directly and cleanly.
- Using an external EventEmitter package. This was rejected per the zero-dependency directive.
## Proposed Implementation
### 1. Define the Handler Type
In `sdk/mod.ts`, define the TypeScript type for the invalidation handler:
```typescript
export type InvalidationHandler = (token: string) => void | Promise<void>;
```
### 2. Implement the Internal Event Bus
In the `AuthSdk` class (`sdk/mod.ts`):
- Add a private property to store listeners:
```typescript
private listeners: Map<string, Set<InvalidationHandler>> = new Map();
```
- Implement the `on` method:
```typescript
/**
* Registers a callback to be invoked when a specific event occurs.
* Currently, only the "invalidate" event is supported.
*
* @param event The event name (e.g., "invalidate").
* @param handler The callback function.
*/
on(event: "invalidate", handler: InvalidationHandler): void;
```
- Implement the `off` method:
```typescript
/**
* Unregisters a previously registered callback.
*
* @param event The event name (e.g., "invalidate").
* @param handler The callback function to remove.
*/
off(event: "invalidate", handler: InvalidationHandler): void;
```
### 3. Integrate with Valkey `push` Events
In the `initValkeyClient` method of `AuthSdk` (`sdk/mod.ts`):
- Locate the existing `"push"` event listener for the Valkey client.
- Inside the loop that iterates over `keysToInvalidate` (which represents the raw tokens), add the emission logic.
- Retrieve the `Set` of listeners for the `"invalidate"` event.
- Iterate over the handlers and invoke them safely using a `try/catch` block and `Promise.resolve(...).catch(...)` to prevent any single failing handler from impacting the others or crashing the SDK.
### 4. Write Unit Tests
In `sdk/mod.test.ts`:
- Write tests to verify that `on` and `off` correctly add and remove handlers.
- Simulate the Valkey `"push"` event (e.g., by creating an instance with a mocked `valkeyClient` or directly testing an internal emit method, or sending mock payloads if the Valkey client is mocked).
- Verify that registered listeners are invoked with the correct token.
- Verify that a throwing or rejecting handler does not disrupt the execution of subsequent handlers.

View File

@ -0,0 +1,82 @@
# TASK METADATA
- **Target Files:** `server/main.ts`, `server/db.ts`, `server/auth-session.ts`
- **Core Objective:** Implement Ingress Grant Vector Injection in
`/api/forward-auth` to resolve `X-Forwarded-Host` against registered apps,
evaluate RBAC grants via Valkey caching, and inject flattened user grant
headers into downstream requests.
- **Dependencies:** Valkey caching infrastructure, database migration for
`domain` column on `apps` table.
- **Additional Important Notes:** Latency is critical; ForwardAuth must resolve
in <30μs using Valkey and must not hit PostgreSQL on every request. Global
Admins bypass app-specific grant checks.
---
## Architectural Considerations & Risks
### Risks
- **Cache Invalidation:** Stale data in Valkey could allow revoked users to
retain access or prevent newly granted users from accessing apps. We must
ensure that grants and app updates properly invalidate or update their
respective Valkey keys.
- **Header Spoofing / Parsing Issues:** Ensure the `X-Forwarded-*` headers are
safely injected and override any existing headers from the client to prevent
privilege escalation (handled by Traefik, but good to be mindful of).
- **Performance Bottlenecks:** While caching resolves database latency, improper
cache interactions (e.g., waiting for multiple sequential round-trips to
Valkey instead of using pipelining or MGET where appropriate) could still add
overhead.
### Alternatives Evaluated
- **Direct PostgreSQL queries:** Rejected due to the high volume of requests
hitting the edge proxy `/api/forward-auth`.
- **JWT Injection:** Instead of flattened headers, injecting a signed JWT was
considered. However, flattened headers (`X-Forwarded-User-Id`,
`X-Forwarded-Scopes`) are native to Traefik ForwardAuth and simpler for
downstream SSR hydration without requiring public key distribution to every
downstream app.
## Proposed Implementation
### 1. Database Schema Update (`server/db.ts`)
- Modify `initDb()` to add the `domain` column to the `apps` table:
`ALTER TABLE apps ADD COLUMN IF NOT EXISTS domain TEXT;`.
- Ensure application seeding or registry logic accounts for the new `domain`
field if necessary.
### 2. Caching Strategy Implementation
- Implement helper functions to fetch from Valkey or fallback to DB and populate
Valkey.
- **App Resolution Cache (`auth:app_by_host:<host>`):**
- Match `X-Forwarded-Host` against `apps.domain`.
- Fallback: Match `apps.name` against the first subdomain segment.
- Cache the resulting `app_id` and `name` in Valkey as JSON.
- **Grants Cache (`auth:grants:<userId>:<appId>`):**
- Cache the user's roles for the specific app.
### 3. ForwardAuth Endpoint Logic (`server/main.ts`)
- Update `GET /api/forward-auth` to:
1. Extract `X-Forwarded-Host` from the request.
2. Resolve the target application via the App Resolution Cache (Valkey). If no
app matches, decide whether to allow pass-through without scopes or
strictly deny (typically 403 for protected routes without an app, or allow
pass-through if unmapped). Assuming strict mapping per requirements, return
403 or 404 if not found.
3. Validate the user session (existing logic).
4. Check if the user is a Global Admin (`isGlobalAdmin`). If yes, authorize
automatically and set `X-Forwarded-Scopes: admin`.
5. For regular users, resolve their scopes for the target app via the Grants
Cache (Valkey).
6. Enforce Default-Deny: If no scopes are found, return 403 Forbidden.
7. On success, inject headers:
- `X-Forwarded-User-Id: <user.id>`
- `X-Forwarded-User-Name: <user.username>`
- `X-Forwarded-Scopes: <comma-separated list of roles>`
- `X-Forwarded-App-Id: <app_id>`
- Return HTTP 200 OK.