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:
Jinjing
2026-03-06 21:45:44 -08:00
committed by GitHub
parent 13e5bf720a
commit 6ab23739ff
10 changed files with 83 additions and 163 deletions
+1
View File
@@ -0,0 +1 @@
flake.lock linguist-generated=true
+5 -3
View File
@@ -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"
+7 -2
View File
@@ -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"
-14
View File
@@ -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
View File
@@ -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]!;
+43
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+4
View File
@@ -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) {
+1 -3
View File
@@ -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 : "";