Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
server-targets-crypto.test.ts4.2 KB · 135 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
 * Pure-function tests for src/lib/server-targets-crypto.ts.
 *
 * Mirror of the workflow-secrets-crypto suite: round-trip, IV randomness,
 * tamper detection, env-name validation, dotenv rendering.
 */

import { describe, it, expect, afterEach } from "bun:test";
import {
  encryptValue,
  decryptValue,
  getMasterKey,
  isValidEnvName,
  renderDotenv,
} from "../lib/server-targets-crypto";

const TEST_KEY =
  "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const original = process.env.SERVER_TARGETS_KEY;

afterEach(() => {
  if (original === undefined) delete process.env.SERVER_TARGETS_KEY;
  else process.env.SERVER_TARGETS_KEY = original;
});

describe("getMasterKey", () => {
  it("returns null when env is unset", () => {
    delete process.env.SERVER_TARGETS_KEY;
    expect(getMasterKey()).toBeNull();
  });

  it("returns null when env is not 32 bytes", () => {
    process.env.SERVER_TARGETS_KEY = "abcd";
    expect(getMasterKey()).toBeNull();
  });

  it("returns 32-byte buffer when env is valid hex", () => {
    process.env.SERVER_TARGETS_KEY = TEST_KEY;
    const key = getMasterKey();
    expect(key).not.toBeNull();
    expect(key!.length).toBe(32);
  });

  it("ignores non-hex input", () => {
    process.env.SERVER_TARGETS_KEY = "zzzz".repeat(16);
    expect(getMasterKey()).toBeNull();
  });
});

describe("encrypt/decrypt round-trip", () => {
  it("recovers the original plaintext", () => {
    process.env.SERVER_TARGETS_KEY = TEST_KEY;
    const enc = encryptValue("hello-server-target");
    expect(enc.ok).toBe(true);
    if (!enc.ok) return;
    const dec = decryptValue(enc.ciphertext);
    expect(dec.ok).toBe(true);
    if (!dec.ok) return;
    expect(dec.plaintext).toBe("hello-server-target");
  });

  it("uses a fresh IV per encryption so identical plaintexts diverge", () => {
    process.env.SERVER_TARGETS_KEY = TEST_KEY;
    const a = encryptValue("same");
    const b = encryptValue("same");
    expect(a.ok && b.ok).toBe(true);
    if (!a.ok || !b.ok) return;
    expect(a.ciphertext).not.toBe(b.ciphertext);
  });

  it("rejects encrypt when key missing", () => {
    delete process.env.SERVER_TARGETS_KEY;
    const enc = encryptValue("anything");
    expect(enc.ok).toBe(false);
  });

  it("detects tampered ciphertext via GCM auth tag", () => {
    process.env.SERVER_TARGETS_KEY = TEST_KEY;
    const enc = encryptValue("payload");
    if (!enc.ok) throw new Error("setup failed");
    // Flip a byte inside the ciphertext portion (after IV+tag) — base64
    // decode → mutate → re-encode.
    const buf = Buffer.from(enc.ciphertext, "base64");
    buf[buf.length - 1] ^= 0xff;
    const tampered = buf.toString("base64");
    const dec = decryptValue(tampered);
    expect(dec.ok).toBe(false);
  });

  it("rejects too-short blob", () => {
    process.env.SERVER_TARGETS_KEY = TEST_KEY;
    const dec = decryptValue(Buffer.from("short").toString("base64"));
    expect(dec.ok).toBe(false);
  });
});

describe("isValidEnvName", () => {
  it("accepts conventional env names", () => {
    expect(isValidEnvName("FOO")).toBe(true);
    expect(isValidEnvName("FOO_BAR")).toBe(true);
    expect(isValidEnvName("_PRIVATE")).toBe(true);
    expect(isValidEnvName("A1_B2")).toBe(true);
  });

  it("rejects lowercase, dashes, leading digits, empty", () => {
    expect(isValidEnvName("foo")).toBe(false);
    expect(isValidEnvName("FOO-BAR")).toBe(false);
    expect(isValidEnvName("1FOO")).toBe(false);
    expect(isValidEnvName("")).toBe(false);
    expect(isValidEnvName(undefined)).toBe(false);
    expect(isValidEnvName(null)).toBe(false);
  });
});

describe("renderDotenv", () => {
  it("renders alphabetised KEY='value' lines", () => {
    const out = renderDotenv({ BAR: "two", FOO: "one" });
    expect(out).toBe("BAR='two'\nFOO='one'\n");
  });

  it("escapes embedded single quotes", () => {
    const out = renderDotenv({ FOO: "a'b" });
    // a'b → 'a'\''b'  — POSIX single-quote escape.
    expect(out).toBe("FOO='a'\\''b'\n");
  });

  it("returns empty string for empty map", () => {
    expect(renderDotenv({})).toBe("");
  });

  it("never leaves a value unquoted", () => {
    const out = renderDotenv({ KEY: "has spaces and $vars" });
    expect(out).toBe("KEY='has spaces and $vars'\n");
  });
});