Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .agents/skills/UNSLOP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: unslop
description: Cut AI tells from any writing. Must always apply.
disable-model-invocation: true
---

# Unslop

Edit text to remove AI patterns.

## Process

1. Scan for the patterns below.
2. Rewrite. Preserve meaning, match intended tone.

## Patterns to detect and fix

Rule numbers are stable ids that other skills cite. A removed rule leaves a gap.

### Content

3. **Superficial -ing phrases.** "highlighting...", "ensuring...", "reflecting...", "showcasing...", "fostering...". Delete or expand with real sources.
5. **Vague attributions.** "Experts believe", "Industry reports suggest", "Some critics argue". Name the source or delete.

### Language

7. **AI vocabulary.** Additionally, crucial, delve, enduring, enhance, fostering, garner, interplay, intricate, landscape (abstract), pivotal, showcase, tapestry (abstract), testament, underscore, vibrant. Replace with plain words.
8. **Fancy ways to say "is".** "serves as", "stands as", "boasts", "features". Just say "is" or "has".
9. **"Not just X, but Y."** State the point directly instead.
10. **Rule of three.** Forcing ideas into groups of three. Use the natural number.
11. **Synonym cycling.** Protagonist, main character, central figure, hero all in one paragraph. Pick one, repeat it.
12. **False ranges.** "from X to Y" where X and Y aren't on a meaningful scale. List topics directly.

### Style

13. **Em dash overuse.** Avoid em dashes entirely. Use periods or commas only (no parentheses, no en dashes, no hyphen-as-dash substitutes). If a thought needs separation, end the sentence or use a comma.
14. **Colon overuse.** Colons are fine before a list or example. Not as mid-sentence connectors. "If you're coming from traditional automation: instead of registering event handlers, you describe conditions" adds nothing with the colon. Rewrite to let the point stand on its own without comparison framing. "Describing when the scheduler should fire works best as plain English." Same meaning, no crutch punctuation.
15. **Boldface overuse.** Don't bold every proper noun or acronym.
16. **Inline-header lists.** The tell is a bold label and colon that restates the line: "**Performance:** Performance improved...". Convert those to prose. A bold lead-in that ends in a period, names the item, and is followed by genuinely new detail ("**Schema in TypeScript.** Tables live in one file.") is fine, not a tell.
17. **Title case headings.** Use sentence case.
18. **Decorative emojis.** Remove from headings and bullets.
19. **Curly quotes.** Replace with straight quotes.

### Communication artifacts

20. **Chatbot phrases.** "I hope this helps!", "Let me know if...", "Of course!", "Certainly!", "Found the smoking gun!" Remove.
22. **Sycophantic tone.** "Great question! You're absolutely right!" Respond directly.

### Filler

23. **Filler phrases.** "In order to" becomes "To". "Due to the fact that" becomes "Because". "It is important to note that" gets deleted.
24. **Excessive hedging.** "could potentially possibly be argued that it might" becomes "may".
25. **Generic conclusions.** "The future looks bright." State specific plans or facts.

### Jargon

26. **Abstract metaphor nouns.** Substrate, wedge, vector, locus, vantage, nexus, primitive (as noun), harness (as metaphor), surface (as in "API surface"), bedrock, scaffolding (as metaphor), modality, paradigm, gold-plating, ratchet (as metaphor), evacuate (for moving code), endgame, north star, flywheel. These read as technical but usually have a plainer concrete word. "Substrate" becomes "base". "Wedge in" becomes "add". "Vector" becomes "way" or "method". "Gold-plating" becomes "more than the job needs". "Ratchet" becomes the mechanism's real name or "a limit that only tightens". "Evacuate" becomes "move out". "Endgame" becomes "the last phase". Pick the concrete word.

### Plain speech

27. **Say what it does, not how it feels.** "the database stays close at hand", "SQL you can read", "types that follow your schema" name a feeling. The fix names the mechanism or a number: "`.toSQL()` returns the exact string sent to the database", "a column rename fails the build". Ask what the sentence tells the reader to do or know, then write that. If you can't restate it as a concrete instruction, fact, or number, cut it. One more check: if the sentence could appear unchanged in another project's docs, it says nothing about this one. Cut it.
28. **Shorten or split dense sentences.** If the reader has to backtrack to parse a sentence, break it in two or drop clauses. One idea per sentence.
29. **Active voice.** Prefer it. Catch "is/are/was/were + past participle" and name the actor: "queries are validated" becomes "the compiler validates queries", "the file is parsed by the loader" becomes "the loader parses the file". Passive is fine only when the actor is unknown or genuinely doesn't matter.
30. **Cut adverbs, or use a stronger verb.** "runs quickly" becomes "is fast" or the number. "significantly improves" becomes the measured delta. An adverb propping up a weak verb means the verb is wrong.
31. **Prefer the plain word.** "utilize" becomes "use", "leverage" becomes "use", "facilitate" becomes "help", "numerous" becomes "many", "in the event that" becomes "if". The fancier synonym is rarely clearer.
32. **Mannered prose.** Metaphor or flourish where a literal phrase exists: aphorisms ("wire it or delete it"), rhetorical fragments for effect, personified code ("the plan holds it"), figurative verbs ("rides along", "stands on"), stock framing phrases. "A dial worth turning" becomes "a parameter worth varying". Say what you mean. Rule 26 covers the metaphor nouns.
33. **Over-compression.** Dropped articles, verbless fragments, symbol-speak, and abbreviations that make the reader decode instead of read. "Parser rejects bad date → exit 2, no write" becomes "The parser rejects a bad date, exits with code 2, and writes nothing." Write whole sentences with their articles and verbs, and spell out arrows and abbreviations.
44 changes: 44 additions & 0 deletions .github/workflows/experimental.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Build and Publish experimental

on:
push:
branches:
- experimental

jobs:
build-and-publish:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Bun
uses: oven-sh/setup-bun@v1

- name: Install dependencies and build web app
run: |
cd apps/web
bun install
bun run build

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Build and push multi-architecture Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ secrets.DOCKER_USERNAME }}/dashwise:experimental
8 changes: 8 additions & 0 deletions apps/backend/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ paths:
- description
- iconUrl
additionalProperties: false
/links/home/groups/cleanup:
post:
tags:
- links
summary: Delete unused home link groups
responses:
"200":
$ref: "#/components/responses/JsonOk"
/links/home/groups:
get:
tags:
Expand Down
71 changes: 43 additions & 28 deletions apps/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
registerSessionConnection,
subscribeActivity,
} from "./lib/activity";
import { ensureSession } from "./lib/data/sessions";

import { authenticateDeviceSocket, closeDeviceRequest, createDeviceRequest } from "./lib/device-code";
import { jobsApi, registerJobsCron } from "./jobs/index";
import { startPocketbase } from "./pocketbase";
import { createLogger } from "./lib/logger";
Expand All @@ -20,7 +21,7 @@ import { getNotifications } from "./lib/data/notifications/items";
import { listIntegrations } from "./lib/data/integrations";
import { getUpcomingEvents } from "./lib/calendar";
import { systemAgentClient } from "./lib/systemAgent";
import { readAuth, readSessionMetadata, requireAuth } from "./routes/shared";
import { requireAuth } from "./routes/shared";
import authRoute from "./routes/auth.route";
import sessionsRoute from "./routes/sessions.route";
import systemRoute from "./routes/system.route";
Expand Down Expand Up @@ -70,21 +71,6 @@ app.use("*", async (c, next) => {

app.use("*", cors({ origin: "*" }));

// Session identity is deliberately independent from the auth token. Touch the
// current device on every authenticated API request that carries its stable id.
app.use("/api/v1/*", async (c, next) => {
const auth = readAuth(c);
if (auth.token && auth.sessionId) {
try {
const { pb, userId } = await requireAuth(auth);
await ensureSession(pb, userId, auth.sessionId, readSessionMetadata(c));
} catch {
// The route handler remains responsible for returning auth errors. This
// middleware should not turn a missing/expired session touch into one.
}
}
await next();
});

app.route("/", authRoute);
app.route("/", sessionsRoute);
Expand All @@ -93,7 +79,40 @@ app.route("/", dataRoute);

app.get("/health", (c) => c.json({ status: "ok" }));

app.get("/api/v1/activity", upgradeWebSocket((c) => {
// Device login never accepts credentials in the URL. The first WebSocket frame
// is only a protocol acknowledgement; the server owns the hidden secret.
app.get("/api/v1/auth/device-code", upgradeWebSocket((c) => {
let requestId = "";
return {
onOpen(_event, ws) {
try {
if (config.ENVIRONMENT === "production" && !["https", "wss"].includes((c.req.header("x-forwarded-proto") ?? new URL(c.req.url).protocol.replace(":", "")))) {
ws.close(1008, "HTTPS is required");
return;
}
const request = createDeviceRequest(ws, c.req.header("x-forwarded-for")?.split(",")[0] ?? "unknown", c.req.header("user-agent") ?? "unknown");
requestId = request.id;
ws.send(JSON.stringify({ type: "device-code", code: request.code, secret: request.secret, requestId: request.id, expiresAt: request.expiresAt }));
} catch (error) {
ws.send(JSON.stringify({ type: "error", message: error instanceof Error ? error.message : "Device login unavailable" }));
ws.close(1013, "Device login unavailable");
}
},
onMessage(event, ws) {
try {
const message = JSON.parse(String(event.data));
if (message.type === "authenticate" && requestId && authenticateDeviceSocket(requestId, String(message.secret ?? ""))) {
ws.send(JSON.stringify({ type: "authenticated" }));
} else {
ws.close(1008, "Invalid device login");
}
} catch { ws.close(1008, "Invalid device login"); }
},
onClose() { if (requestId) closeDeviceRequest(requestId); },
};
}));

app.get("/api/v1/activity", upgradeWebSocket((c) => {
let refreshTimer: ReturnType<typeof setInterval> | undefined;
let unsubscribeActivity: (() => void) | undefined;
let unregisterSessionConnection: (() => void) | undefined;
Expand All @@ -103,14 +122,12 @@ app.get("/api/v1/activity", upgradeWebSocket((c) => {
return {
async onOpen(_event, ws) {
const token = c.req.query("token") || "";
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id") || null;
const requestedSessionId = c.req.query("sessionId") || c.req.header("x-session-id") || null;
try {
const { userId, pb } = await requireAuth({ token, sessionId });
const session = await ensureSession(pb, userId, sessionId, readSessionMetadata(c));
if (!session) throw new Error("A valid session id is required");
const { userId, pb, sessionId } = await requireAuth({ token, sessionId: requestedSessionId });
connectedUserId = userId;
connectedSessionId = session.sessionId;
unregisterSessionConnection = registerSessionConnection(userId, session.sessionId, ws);
connectedSessionId = sessionId;
unregisterSessionConnection = registerSessionConnection(userId, sessionId, ws);
let calendarEvents: Array<Record<string, any>> = [];
let calendarRefreshedAt = 0;
let calendarRefresh: Promise<void> | null = null;
Expand Down Expand Up @@ -202,8 +219,7 @@ app.get("/api/v1/monitoring/ssh-hosts/:id/console", upgradeWebSocket((c) => {
const hostId = c.req.param("id") || "";

try {
const { userId, pb } = await requireAuth({ token, sessionId });
await ensureSession(pb, userId, sessionId, readSessionMetadata(c));
const { userId } = await requireAuth({ token, sessionId });
const host = await getMonitoringSshHostById(userId, hostId);
if (!host) {
ws.send(JSON.stringify({ type: "error", message: "SSH host not found" }));
Expand Down Expand Up @@ -291,8 +307,7 @@ app.get("/api/v1/monitoring/hosts/:id/stats/live", upgradeWebSocket((c) => {
const token = c.req.query("token") || c.req.header("Authorization")?.replace(/^Bearer\s+/i, "") || "";
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id") || null;
try {
const { userId, pb } = await requireAuth({ token, sessionId });
await ensureSession(pb, userId, sessionId, readSessionMetadata(c));
const { userId } = await requireAuth({ token, sessionId });
const host = await getSystemAgentHostById(userId, c.req.param("id") || "");
if (!host) {
ws.close(1008, "Monitoring host not found");
Expand Down
89 changes: 89 additions & 0 deletions apps/backend/src/lib/data/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, test } from "bun:test";

import { registerDashwiseSDKConnector } from "../pb/pocketbase";
import { ApiActionError, requireUserAuth } from "./auth";
import { hashSessionToken } from "./sessions";

type AuthFixture = {
refreshCalls: string[];
setRefreshResult: (result: "success" | "unauthorized") => void;
};

function installAuthFixture(): AuthFixture {
const refreshCalls: string[] = [];
let refreshResult: "success" | "unauthorized" = "unauthorized";
let storedToken = "internal-pb-token";
const session = {
id: "record-id",
sessionId: "session-id",
user: "verified-user-id",
pocketbaseToken: storedToken,
lastSeenAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};

const connector = {
async getSuperuserClient() {
return {
collection() {
return {
async getFirstListItem(filter: string) {
if (filter !== `tokenHash = "${hashSessionToken("dws_test-session-token")}"`) throw new Error("not found");
return session;
},
async update(_id: string, value: Record<string, string>) {
storedToken = value.pocketbaseToken;
return session;
},
};
},
};
},
createServerClient() {
let token = "";
return {
authStore: {
get token() { return token; },
save(nextToken: string) { token = nextToken; },
},
collection() {
return {
async authRefresh() {
refreshCalls.push(token);
if (refreshResult === "unauthorized") {
throw new ApiActionError("Unauthorized", 401, { error: "Unauthorized" });
}
return { record: { id: "verified-user-id" }, token };
},
};
},
};
},
};

registerDashwiseSDKConnector(connector as never);
return {
refreshCalls,
setRefreshResult(result) {
refreshResult = result;
},
};
}

describe("requireUserAuth", () => {
test("rejects PocketBase JWTs because only Dashwise session credentials are accepted", async () => {
installAuthFixture();
await expect(requireUserAuth({ token: "eyJhbGciOiJIUzI1NiJ9.payload.signature" })).rejects.toMatchObject({ status: 401 });
});

test("resolves user identity through an active Dashwise session", async () => {
const fixture = installAuthFixture();
fixture.setRefreshResult("success");

await expect(requireUserAuth({ token: "dws_test-session-token" })).resolves.toMatchObject({
userId: "verified-user-id",
sessionId: "session-id",
});
expect(fixture.refreshCalls).toEqual(["internal-pb-token"]);
});
});
Loading
Loading