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
12 changes: 12 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# gitleaks configuration.
# Extends the default ruleset; adds an allowlist for test files, which
# legitimately contain hardcoded FAKE secrets (e.g. HMAC token test fixtures),
# never real credentials. Real config secrets live in gitignored .env files.
[extend]
useDefault = true

[allowlist]
description = "Test files use fake fixture secrets, not real credentials."
paths = [
'''.*\.test\.ts$''',
]
8 changes: 4 additions & 4 deletions specs/mail/threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ Threading authority lives on the outbound side (charter §2). `In-Reply-To` and

## 2. The reply token

Every outbound message (agent reply, auto-response, and any future first-party auto-reply) embeds a signed token in its `Message-ID`. Proposed format:
Every outbound message (agent reply, auto-response, and any future first-party auto-reply) embeds a signed token in its `Message-ID`. Format (implemented in `src/mail/reply-token.ts`):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Disambiguate Agents and Assistants in the outbound-message list.

Use the repository’s reserved terms explicitly: distinguish an Agent reply from an Assistant response or non-AI automated reply.

As per coding guidelines, human support staff are Agents and AI actors are Assistants; do not conflate them in documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/mail/threading.md` at line 11, The outbound-message list in the
threading documentation conflates actor types. Update the wording to explicitly
distinguish human Agent replies from Assistant responses and other non-AI
automated replies, using the repository’s reserved terminology consistently.

Source: Coding guidelines


```
<ht.{conversationId}.{threadId}.{sig}@{mailDomain}>
<ht.{keyId}.{conversationId}.{threadId}.{sig}@{mailDomain}>
```
Comment on lines 13 to 15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced token-format block.

markdownlint-cli2 reports MD040 on Line 13. Use text (or another appropriate language) for the fence.

Proposed fix
-```
+```text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
<ht.{conversationId}.{threadId}.{sig}@{mailDomain}>
<ht.{keyId}.{conversationId}.{threadId}.{sig}@{mailDomain}>
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 13-13: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/mail/threading.md` around lines 13 - 15, Add the text language
identifier to the fenced code block containing the token format, changing its
opening fence to ```text while preserving the existing content.

Source: Linters/SAST tools


where `sig = HMAC(secret, canonical(conversationId, threadId))`, truncated and hex/base32-encoded. This is Helpthread's own design, not derived from any observed system's internals — we only observed a black-box Message-ID *shape*, never a secret or algorithm.
where `sig = base64url( HMAC-SHA256( secret, "{keyId}.{conversationId}.{threadId}" ) )` — the full 32-byte HMAC, base64url-encoded, unpadded (not truncated; the extra bytes are trivial inside a Message-ID and full length is the safest choice). The `keyId` names the signing key and is itself part of the signed payload, so a token's key cannot be swapped without invalidating it. `mailDomain` is not signed (it is not part of threading identity). The id fields are constrained at mint time to `[A-Za-z0-9_-]` (the base64url alphabet, excluding the `.` delimiter), so a well-formed local part splits unambiguously into five segments. This is Helpthread's own design, not derived from any observed system's internals — we only observed a black-box Message-ID *shape*, never a secret or algorithm.

The properties that ARE the spec, independent of encoding:

- **(a) Unguessable without the secret** — not forgeable by an attacker who has seen valid tokens (cf. §3 rule 3).
- **(b) Verifiable offline** — no DB round-trip to detect tampering; pure computation against the signing secret(s).
- **(c) Carries the conversation+thread identity** — a verified token deterministically identifies its conversation/thread; no lookup table of issued tokens required.
- **(d) Rotation-tolerant** — the signing secret must be rotatable without invalidating outstanding tokens, implying a `keyId` alongside the signature. **OPEN QUESTION:** does `keyId` ship in v1, or wait for the first rotation?
- **(d) Rotation-tolerant** — the signing secret must be rotatable without invalidating outstanding tokens. **RESOLVED (HT-12): `keyId` ships in v1.** A keyring has one `current` key (mints and verifies) and zero or more `retired` keys (verify only); rotating means retiring the old key and promoting a new `current`, which never invalidates tokens already in customers' mailboxes. Dropping a key from the ring entirely stops its tokens from verifying.

**Contrast with the observed reference format.** The fixtures show a reference helpdesk emitting Message-IDs shaped like `<FS_reply-{threadId}-{token}@{domain}>` — e.g. `<FS_reply-36-{token}@helpdesk.example.test>` (reply-with-reference.json, `agentReplyEmail.messageId`; the token value in the committed fixtures is a redacted placeholder — the real capability token is never published). Notably `{threadId}` there is a *thread* id (36), not the conversation id (15) — conversation is resolved via the thread's parent, not encoded directly. This is cited only as evidence the "signed token in the outbound Message-ID" pattern works in production (charter §2); Helpthread's `sig` derivation, secret, and truncation are unrelated to whatever that system does internally, which was never observed.

Expand Down
Loading
Loading