mirror of
https://github.com/stablyai/agent-slack.git
synced 2026-09-19 01:06:54 +08:00
fix(nix): harden update script with GitHub token auth and add .gitattributes (#54)
- fix(nix): harden update script with GitHub token auth and add .gitattributes - fix(nix): harden update script with GitHub token auth and add .gitattributes - fix issues - fix issues - fix issues - fix issues
This commit is contained in:
@@ -0,0 +1 @@
|
||||
flake.lock linguist-generated=true
|
||||
@@ -15,16 +15,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31
|
||||
|
||||
- name: Update nix sources
|
||||
run: ./scripts/update-nix-sources.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
|
||||
with:
|
||||
commit-message: "chore(nix): update release hashes"
|
||||
title: "chore(nix): update release hashes"
|
||||
|
||||
@@ -14,7 +14,12 @@ if ! command -v nix >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag="$(curl -fsSL "$latest_api" | jq -r '.tag_name')"
|
||||
curl_opts=(-fsSL)
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
curl_opts+=(-H "Authorization: token $GITHUB_TOKEN")
|
||||
fi
|
||||
|
||||
tag="$(curl "${curl_opts[@]}" "$latest_api" | jq -r '.tag_name')"
|
||||
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
||||
echo "error: unable to resolve latest release tag" >&2
|
||||
exit 1
|
||||
@@ -22,7 +27,7 @@ fi
|
||||
|
||||
version="${tag#v}"
|
||||
checksums_url="https://github.com/${repo}/releases/download/${tag}/checksums-sha256.txt"
|
||||
checksums="$(curl -fsSL "$checksums_url")"
|
||||
checksums="$(curl "${curl_opts[@]}" "$checksums_url")"
|
||||
|
||||
get_sri() {
|
||||
local asset="$1"
|
||||
|
||||
@@ -78,20 +78,6 @@ Run `agent-slack --help` (or `agent-slack <command> --help`) for the full option
|
||||
- `--workspace <url-or-unique-substring>` (needed for channel _names_ across multiple workspaces)
|
||||
- `--ts <seconds>.<micros>` (required for channel targets)
|
||||
|
||||
- `agent-slack message edit <target> <text>`
|
||||
- URL target edits that exact message.
|
||||
- Channel target requires `--ts`.
|
||||
- Options:
|
||||
- `--workspace <url-or-unique-substring>` (needed for channel _names_ across multiple workspaces)
|
||||
- `--ts <seconds>.<micros>` (required for channel targets)
|
||||
|
||||
- `agent-slack message delete <target>`
|
||||
- URL target deletes that exact message.
|
||||
- Channel target requires `--ts`.
|
||||
- Options:
|
||||
- `--workspace <url-or-unique-substring>` (needed for channel _names_ across multiple workspaces)
|
||||
- `--ts <seconds>.<micros>` (required for channel targets)
|
||||
|
||||
- `agent-slack message react add <target> <emoji>`
|
||||
- `agent-slack message react remove <target> <emoji>`
|
||||
- Options (channel mode):
|
||||
|
||||
+3
-40
@@ -1,9 +1,10 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
||||
import { homedir, platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { queryReadonlySqlite } from "./firefox-profile.ts";
|
||||
import { decryptChromiumCookieValue } from "./chromium-cookie.ts";
|
||||
import { isRecord } from "../lib/object-type-guards.ts";
|
||||
|
||||
type BraveExtractedTeam = { url: string; name?: string; token: string };
|
||||
|
||||
@@ -49,10 +50,6 @@ function teamsScript(): string {
|
||||
`;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function toBraveTeam(value: unknown): BraveExtractedTeam | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
@@ -117,40 +114,6 @@ function getSafeStoragePasswords(): string[] {
|
||||
return passwords;
|
||||
}
|
||||
|
||||
function decryptChromiumCookieValue(data: Buffer, password: string): string {
|
||||
if (!data || data.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const salt = Buffer.from("saltysalt", "utf8");
|
||||
const iv = Buffer.alloc(16, " ");
|
||||
const key = pbkdf2Sync(password, salt, 1003, 16, "sha1");
|
||||
|
||||
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
||||
decipher.setAutoPadding(true);
|
||||
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
||||
const marker = Buffer.from("xoxd-");
|
||||
const idx = plain.indexOf(marker);
|
||||
if (idx === -1) {
|
||||
return plain.toString("utf8");
|
||||
}
|
||||
|
||||
let end = idx;
|
||||
while (end < plain.length) {
|
||||
const b = plain[end]!;
|
||||
if (b < 0x21 || b > 0x7e) {
|
||||
break;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
const rawToken = plain.subarray(idx, end).toString("utf8");
|
||||
try {
|
||||
return decodeURIComponent(rawToken);
|
||||
} catch {
|
||||
return rawToken;
|
||||
}
|
||||
}
|
||||
|
||||
async function extractCookieDFromBrave(): Promise<string> {
|
||||
if (!existsSync(BRAVE_COOKIES_DB)) {
|
||||
throw new Error(`Brave Cookies DB not found: ${BRAVE_COOKIES_DB}`);
|
||||
@@ -185,7 +148,7 @@ async function extractCookieDFromBrave(): Promise<string> {
|
||||
|
||||
for (const password of passwords) {
|
||||
try {
|
||||
const decrypted = decryptChromiumCookieValue(data, password);
|
||||
const decrypted = decryptChromiumCookieValue(data, password, 1003);
|
||||
const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
|
||||
if (match) {
|
||||
return match[0]!;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
||||
|
||||
export function decryptChromiumCookieValue(
|
||||
data: Buffer,
|
||||
password: string,
|
||||
iterations: number,
|
||||
): string {
|
||||
if (!data || data.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (iterations < 1) {
|
||||
throw new RangeError(`iterations must be >= 1, got ${iterations}`);
|
||||
}
|
||||
|
||||
const salt = Buffer.from("saltysalt", "utf8");
|
||||
const iv = Buffer.alloc(16, " ");
|
||||
const key = pbkdf2Sync(password, salt, iterations, 16, "sha1");
|
||||
|
||||
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
||||
decipher.setAutoPadding(true);
|
||||
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
||||
const marker = Buffer.from("xoxd-");
|
||||
const idx = plain.indexOf(marker);
|
||||
if (idx === -1) {
|
||||
return plain.toString("utf8");
|
||||
}
|
||||
|
||||
let end = idx;
|
||||
while (end < plain.length) {
|
||||
const b = plain[end]!;
|
||||
if (b < 0x21 || b > 0x7e) {
|
||||
break;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
const rawToken = plain.subarray(idx, end).toString("utf8");
|
||||
try {
|
||||
return decodeURIComponent(rawToken);
|
||||
} catch {
|
||||
return rawToken;
|
||||
}
|
||||
}
|
||||
+18
-97
@@ -10,61 +10,13 @@ import {
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
||||
import { createDecipheriv, randomUUID } from "node:crypto";
|
||||
import { homedir, platform, tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { findKeysContaining } from "../lib/leveldb-reader.js";
|
||||
|
||||
type SqliteRow = Record<string, unknown>;
|
||||
|
||||
function isMissingBunSqliteModule(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
}
|
||||
const err = error as { code?: unknown; message?: unknown };
|
||||
const code = typeof err.code === "string" ? err.code : "";
|
||||
const message = typeof err.message === "string" ? err.message : "";
|
||||
|
||||
if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
|
||||
return true;
|
||||
}
|
||||
if (!message.includes("bun:sqlite")) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
message.includes("Cannot find module") ||
|
||||
message.includes("Unknown builtin module") ||
|
||||
message.includes("unsupported URL scheme") ||
|
||||
message.includes("Only URLs with a scheme in")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query a SQLite database in read-only mode.
|
||||
* Uses bun:sqlite when running under Bun, falls back to node:sqlite (Node >= 22.5).
|
||||
*/
|
||||
async function queryReadonlySqlite(dbPath: string, sql: string): Promise<SqliteRow[]> {
|
||||
try {
|
||||
const { Database } = await import("bun:sqlite");
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
try {
|
||||
return db.query(sql).all() as SqliteRow[];
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isMissingBunSqliteModule(error)) {
|
||||
throw error;
|
||||
}
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
try {
|
||||
return db.prepare(sql).all() as SqliteRow[];
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
import { isRecord } from "../lib/object-type-guards.ts";
|
||||
import { queryReadonlySqlite } from "./firefox-profile.ts";
|
||||
import { decryptChromiumCookieValue } from "./chromium-cookie.ts";
|
||||
|
||||
type DesktopTeam = { url: string; name?: string; token: string };
|
||||
|
||||
@@ -164,10 +116,6 @@ function getSlackPaths(): { leveldbDir: string; cookiesDb: string; baseDir: stri
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function toDesktopTeam(value: unknown): DesktopTeam | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
@@ -364,40 +312,6 @@ function getSafeStoragePasswords(prefix: string): string[] {
|
||||
throw new Error("Could not read Safe Storage password from desktop keychain.");
|
||||
}
|
||||
|
||||
function decryptChromiumCookieValue(data: Buffer, password: string): string {
|
||||
if (!data || data.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const salt = Buffer.from("saltysalt", "utf8");
|
||||
const iv = Buffer.alloc(16, " ");
|
||||
const key = pbkdf2Sync(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
|
||||
|
||||
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
||||
decipher.setAutoPadding(true);
|
||||
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
||||
const marker = Buffer.from("xoxd-");
|
||||
const idx = plain.indexOf(marker);
|
||||
if (idx === -1) {
|
||||
return plain.toString("utf8");
|
||||
}
|
||||
|
||||
let end = idx;
|
||||
while (end < plain.length) {
|
||||
const b = plain[end]!;
|
||||
if (b < 0x21 || b > 0x7e) {
|
||||
break;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
const rawToken = plain.subarray(idx, end).toString("utf8");
|
||||
try {
|
||||
return decodeURIComponent(rawToken);
|
||||
} catch {
|
||||
return rawToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a Chromium cookie on Windows using DPAPI + AES-256-GCM.
|
||||
*
|
||||
@@ -413,18 +327,25 @@ function decryptCookieWindows(encrypted: Buffer, slackDataDir: string): string {
|
||||
if (!existsSync(localStatePath)) {
|
||||
throw new Error(`Local State file not found: ${localStatePath}`);
|
||||
}
|
||||
const localState = JSON.parse(readFileSync(localStatePath, "utf8"));
|
||||
if (!localState.os_crypt?.encrypted_key) {
|
||||
let localState: unknown;
|
||||
try {
|
||||
localState = JSON.parse(readFileSync(localStatePath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse Local State file: ${localStatePath}`, { cause: error });
|
||||
}
|
||||
const osCrypt = isRecord(localState) ? localState.os_crypt : undefined;
|
||||
if (!isRecord(osCrypt) || typeof osCrypt.encrypted_key !== "string") {
|
||||
throw new Error("No os_crypt.encrypted_key in Local State");
|
||||
}
|
||||
const encKeyFull = Buffer.from(localState.os_crypt.encrypted_key, "base64");
|
||||
const encKeyFull = Buffer.from(osCrypt.encrypted_key as string, "base64");
|
||||
// Skip "DPAPI" prefix (5 bytes)
|
||||
const encKeyBlob = encKeyFull.subarray(5);
|
||||
|
||||
// Decrypt AES key via Windows DPAPI using PowerShell
|
||||
const encKeyFile = join(tmpdir(), `as-key-enc-${Date.now()}.bin`);
|
||||
const decKeyFile = join(tmpdir(), `as-key-dec-${Date.now()}.bin`);
|
||||
writeFileSync(encKeyFile, encKeyBlob);
|
||||
const id = randomUUID();
|
||||
const encKeyFile = join(tmpdir(), `as-key-enc-${id}.bin`);
|
||||
const decKeyFile = join(tmpdir(), `as-key-dec-${id}.bin`);
|
||||
writeFileSync(encKeyFile, encKeyBlob, { mode: 0o600 });
|
||||
try {
|
||||
// Escape single quotes for PowerShell single-quoted strings (' → '')
|
||||
const psEncKeyFile = encKeyFile.replaceAll("'", "''");
|
||||
@@ -542,7 +463,7 @@ async function extractCookieDFromSlackCookiesDb(
|
||||
|
||||
for (const password of passwords) {
|
||||
try {
|
||||
const decrypted = decryptChromiumCookieValue(data, password);
|
||||
const decrypted = decryptChromiumCookieValue(data, password, IS_LINUX ? 1 : 1003);
|
||||
const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
|
||||
if (match) {
|
||||
return match[0]!;
|
||||
|
||||
+1
-4
@@ -1,5 +1,6 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { isRecord } from "./object-type-guards.ts";
|
||||
|
||||
export async function readJsonFile<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
@@ -21,7 +22,3 @@ export async function writeJsonFile(path: string, data: unknown): Promise<void>
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,10 @@ export async function fetchChannelHistory(
|
||||
break;
|
||||
}
|
||||
|
||||
if (!resp.has_more) {
|
||||
break;
|
||||
}
|
||||
|
||||
const last = messages.at(-1);
|
||||
const nextLatest = isRecord(last) ? getString(last.ts) : undefined;
|
||||
if (!nextLatest || nextLatest === cursorLatest) {
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* Rich-text block rendering helpers extracted from render.ts.
|
||||
*/
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
import { isRecord } from "../lib/object-type-guards.ts";
|
||||
|
||||
function getString(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
|
||||
Reference in New Issue
Block a user