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
atom-feed.ts5.3 KB · 173 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
 * Block J19 — Atom feed renderer.
 *
 * Produces a valid Atom 1.0 XML document from a typed description of a
 * feed. Zero-IO, zero dependencies — routes fetch commits/releases/issues
 * from Drizzle and git, shape them into `AtomEntry` rows, then call
 * `renderAtomFeed` to produce the response body.
 *
 * We deliberately avoid pulling in a full XML library: Atom is a small
 * enough format that a careful `escapeXml` + string template is sufficient
 * and leaves no surface for supply-chain surprises.
 */

export interface AtomAuthor {
  name: string;
  email?: string;
}

export interface AtomEntry {
  /** Stable globally-unique ID (e.g. `tag:host,2026:repo/owner/name/commit/<sha>`). */
  id: string;
  title: string;
  /** Canonical permalink for the entry (becomes `<link rel="alternate">`). */
  href: string;
  /** ISO-8601 UTC timestamp. Used for both `<updated>` and `<published>`. */
  updatedAt: string;
  /** Short plaintext summary. Rendered inside `<summary type="text">`. */
  summary?: string;
  /** Optional long-form content. Rendered inside `<content type="html">`. */
  contentHtml?: string;
  author?: AtomAuthor;
}

export interface AtomFeedInput {
  /** Feed-wide unique ID. */
  id: string;
  title: string;
  subtitle?: string;
  /** Absolute URL of the feed itself (becomes `<link rel="self">`). */
  selfHref: string;
  /** Absolute URL of the HTML page the feed represents. */
  alternateHref?: string;
  /** ISO-8601 UTC. Defaults to the newest entry's `updatedAt`, or "now". */
  updatedAt?: string;
  entries: AtomEntry[];
}

/**
 * Escape the five XML special characters so they render safely inside
 * element text + attribute values. Accepts arbitrary input.
 */
export function escapeXml(input: string): string {
  if (!input) return "";
  return input
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

/**
 * Coerce an input to a valid ISO-8601 UTC date string. Falls back to the
 * unix epoch if the input can't be parsed — feeds stay valid even with
 * junk inputs.
 */
export function toIsoUtc(input: string | Date | null | undefined): string {
  if (input instanceof Date) {
    const t = input.getTime();
    return Number.isFinite(t) ? new Date(t).toISOString() : "1970-01-01T00:00:00.000Z";
  }
  if (typeof input === "string" && input) {
    const t = Date.parse(input);
    if (Number.isFinite(t)) return new Date(t).toISOString();
  }
  return "1970-01-01T00:00:00.000Z";
}

function pickFeedUpdated(input: AtomFeedInput): string {
  if (input.updatedAt) return toIsoUtc(input.updatedAt);
  if (input.entries.length > 0) {
    // Pick the newest entry updatedAt.
    let newest = -Infinity;
    for (const e of input.entries) {
      const t = Date.parse(e.updatedAt);
      if (Number.isFinite(t) && t > newest) newest = t;
    }
    if (newest > -Infinity) return new Date(newest).toISOString();
  }
  return new Date().toISOString();
}

function renderAuthor(a: AtomAuthor): string {
  const lines = [`    <name>${escapeXml(a.name || "unknown")}</name>`];
  if (a.email) lines.push(`    <email>${escapeXml(a.email)}</email>`);
  return `  <author>\n${lines.join("\n")}\n  </author>`;
}

function renderEntry(e: AtomEntry): string {
  const parts = [
    "  <entry>",
    `    <id>${escapeXml(e.id)}</id>`,
    `    <title>${escapeXml(e.title || "(untitled)")}</title>`,
    `    <link rel="alternate" href="${escapeXml(e.href)}"/>`,
    `    <updated>${toIsoUtc(e.updatedAt)}</updated>`,
    `    <published>${toIsoUtc(e.updatedAt)}</published>`,
  ];
  if (e.author) {
    parts.push(
      `    <author>`,
      `      <name>${escapeXml(e.author.name || "unknown")}</name>`,
      ...(e.author.email
        ? [`      <email>${escapeXml(e.author.email)}</email>`]
        : []),
      `    </author>`
    );
  }
  if (e.summary) {
    parts.push(
      `    <summary type="text">${escapeXml(e.summary)}</summary>`
    );
  }
  if (e.contentHtml) {
    parts.push(
      `    <content type="html">${escapeXml(e.contentHtml)}</content>`
    );
  }
  parts.push("  </entry>");
  return parts.join("\n");
}

/**
 * Render a full Atom 1.0 document. The output is UTF-8, XML-declaration-
 * prefixed, and safe to return directly with
 * `Content-Type: application/atom+xml; charset=utf-8`.
 */
export function renderAtomFeed(input: AtomFeedInput): string {
  const updated = pickFeedUpdated(input);
  const lines: string[] = [
    '<?xml version="1.0" encoding="utf-8"?>',
    '<feed xmlns="http://www.w3.org/2005/Atom">',
    `  <id>${escapeXml(input.id)}</id>`,
    `  <title>${escapeXml(input.title)}</title>`,
  ];
  if (input.subtitle) {
    lines.push(`  <subtitle>${escapeXml(input.subtitle)}</subtitle>`);
  }
  lines.push(`  <updated>${updated}</updated>`);
  lines.push(`  <link rel="self" href="${escapeXml(input.selfHref)}"/>`);
  if (input.alternateHref) {
    lines.push(
      `  <link rel="alternate" href="${escapeXml(input.alternateHref)}"/>`
    );
  }
  for (const entry of input.entries) {
    lines.push(renderEntry(entry));
  }
  lines.push("</feed>");
  return lines.join("\n") + "\n";
}

/** Mime-type header for Atom responses. */
export const ATOM_CONTENT_TYPE = "application/atom+xml; charset=utf-8";

export const __internal = {
  escapeXml,
  toIsoUtc,
  pickFeedUpdated,
  renderAuthor,
  renderEntry,
  renderAtomFeed,
  ATOM_CONTENT_TYPE,
};