Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
228 changes: 228 additions & 0 deletions docs/ZEROSSL_HEADER_AUTH_SPEC.md

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions docs/ZEROSSL_HEADER_AUTH_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
STATUS: PROGRESS
WHAT: Verified the requested branch and base commit; beginning official-doc and repository-flow research.
NEED: nothing
NEXT: Verify ZeroSSL's current authentication contract and enumerate every Dashmate call, caller, helper, and test.

STATUS: FINDING
WHAT: ZeroSSL's current public REST overview and endpoint pages still require access_key query parameters; no public official page documenting Authorization: ApiKey was discoverable.
NEED: nothing
NEXT: Record the evidence conflict explicitly and design a conditional implementation gate around the newer announcement.

STATUS: FINDING
WHAT: Confirmed seven production wrappers and no existing wrapper/requestApi unit coverage; revokeCertificate currently has no repository caller.
NEED: nothing
NEXT: Compare authentication-injection seams and specify parameterized red-to-green request-contract coverage.

STATUS: PROGRESS
WHAT: Chose centralized header injection in the ZeroSSL-only requestApi helper; endpoint signatures and upstream callers remain unchanged.
NEED: nothing
NEXT: Write the evidence-gated design, exact request matrix, failure analysis, and TDD contract into the spec.

STATUS: DONE
WHAT: Completed the repository-grounded ZeroSSL header-auth spec with official-source conflict, centralized design, forward-port notes, and red-to-green request matrix.
NEED: Canonical ZeroSSL announcement or written support confirmation before implementation.
NEXT: Waiting for parent-orchestrated independent spec review and Ivan's alignment; no code work is authorized.

STATUS: PROGRESS
WHAT: Started independent coherence, feasibility, security, adversarial, and product reviews of the spec in two waves.
NEED: Nothing.
NEXT: Verify and fold review findings, then explain the reviewed plan and request Ivan's alignment before coding.

STATUS: FINDING
WHAT: Review confirmed invalid header errors can echo the key, case-variant Authorization values can combine, and reflected response errors can reach logs.
NEED: Nothing.
NEXT: Specify case-insensitive header ownership, generic local failures, exact-key response redaction, and adversarial tests.

STATUS: FINDING
WHAT: Public ZeroSSL docs still conflict with the supplied announcement; reviewers required a named evidence owner, terminal deferral path, and rollout validation.
NEED: Canonical ZeroSSL announcement or written ZeroSSL support confirmation.
NEXT: Fold the gate and operational decision path into the reviewed spec, then wait for Ivan's alignment and evidence.

STATUS: FINDING
WHAT: Second-pass review found that redaction must precede Error construction and malformed JSON parse errors must be replaced before they can quote reflected key material.
NEED: Nothing.
NEXT: Tighten the response-boundary contract and adversarial tests, then complete the reviewed-plan sync.

STATUS: BLOCKED
WHAT: Completed two-pass multi-agent review and folded verified findings; no production or test code was changed.
NEED: Canonical ZeroSSL announcement or written support confirmation, plus Ivan's explicit alignment with the reviewed plan.
NEXT: After both gates clear, write and run the failing request-contract tests, implement the centralized migration, verify green, and run independent code review.

STATUS: BLOCKED
WHAT: Ivan requested implementation and aligned on proceeding, but the mandatory ZeroSSL contract evidence gate remains unsatisfied; source and tests are untouched.
NEED: Canonical ZeroSSL announcement/account notice showing the exact header, or written ZeroSSL support confirmation for the scoped endpoints.
NEXT: On receipt, start with the failing request-contract tests, then implement, verify, and independently review the code.

STATUS: DONE
WHAT: Verified ZeroSSL's official 2026-08-17 update: header auth is live, URL access_key is deprecated, and the exact exclusive prefix is Authorization: ApiKey.
NEED: Nothing.
NEXT: Begin TDD implementation with the focused request-contract test failing against untouched production code.

STATUS: PROGRESS
WHAT: Added the focused request contract and confirmed 17 failures against untouched production code, including all seven credential-bearing URLs and error-boundary leaks.
NEED: Nothing.
NEXT: Implement centralized header injection and remove access_key from the seven endpoint URLs, then rerun the identical test.

STATUS: FINDING
WHAT: Independent code review found reflected error member names and leading key whitespace were not covered; both new regression cases failed before the follow-up fix.
NEED: Nothing.
NEXT: Redact member names, reject leading/trailing key whitespace, and rerun focused tests and lint.

STATUS: DONE
WHAT: Implemented header-only auth for all seven wrappers; 20 focused and 32 ZeroSSL unit tests pass, Dashmate lint has zero errors, and production access_key search is empty.
NEED: Full Dashmate unit discovery still requires the missing wasm-dpp build, which is blocked by a confirmed sccache Operation not permitted failure.
NEXT: Complete final post-fix review and hand off the uncommitted implementation; deployment still requires the spec's header-only read-only smoke check.

STATUS: DONE
WHAT: Independent post-fix security and test review is clean after strengthening Certificate conversion and sanitized-error shape assertions.
NEED: Nothing for the scoped implementation; the full-suite WASM/sccache prerequisite and deployment smoke check remain explicit handoff items.
NEXT: Waiting for commit/PR instructions; no commit, push, PR, or v4.2-dev change was made.

STATUS: DONE
WHAT: Rebuilt wasm-dpp with Homebrew LLVM and ran the complete Dashmate unit suite outside the sandbox; all 178 tests pass.
NEED: Nothing for repository verification; the deployment smoke check remains an operational handoff.
NEXT: Waiting for commit/PR instructions; no commit, push, PR, or v4.2-dev change was made.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Remove the internal migration progress log

This file is a chronological implementation log rather than durable project documentation. It preserves superseded states such as BLOCKED, says that source and tests were untouched, claims no commit or PR was made, and refers to a migration specification that the final commit removes. Those statements contradict the completed implementation and can mislead readers; delete this status artifact together with the removed specification.

source: ['codex']

4 changes: 2 additions & 2 deletions packages/dashmate/src/ssl/zerossl/cancelCertificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import requestApi from './requestApi.js';
* @return {Promise<Certificate>}
*/
export default async function cancelCertificate(apiKey, id) {
const url = `https://api.zerossl.com/certificates/${id}/cancel?access_key=${apiKey}`;
const url = `https://api.zerossl.com/certificates/${id}/cancel`;

const requestOptions = {
method: 'POST',
Expand All @@ -18,5 +18,5 @@ export default async function cancelCertificate(apiKey, id) {
},
};

return requestApi(url, requestOptions);
return requestApi(apiKey, url, requestOptions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default async function createZeroSSLCertificate(
certificate_csr: csr,
});

const url = `https://api.zerossl.com/certificates?access_key=${apiKey}`;
const url = 'https://api.zerossl.com/certificates';

const requestOptions = {
method: 'POST',
Expand All @@ -32,7 +32,7 @@ export default async function createZeroSSLCertificate(
},
};

const data = await requestApi(url, requestOptions);
const data = await requestApi(apiKey, url, requestOptions);

return new Certificate(data);
}
4 changes: 2 additions & 2 deletions packages/dashmate/src/ssl/zerossl/downloadCertificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
import requestApi from './requestApi.js';

export default async function downloadCertificate(id, apiKey) {
const url = `https://api.zerossl.com/certificates/${id}/download/return?access_key=${apiKey}`;
const url = `https://api.zerossl.com/certificates/${id}/download/return`;

const requestOptions = {
method: 'GET',
headers: { },
};

const data = await requestApi(url, requestOptions);
const data = await requestApi(apiKey, url, requestOptions);

return `${data['certificate.crt']}\n${data['ca_bundle.crt']}`;
}
4 changes: 2 additions & 2 deletions packages/dashmate/src/ssl/zerossl/getCertificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import requestApi from './requestApi.js';
* @return {Promise<Certificate>}
*/
export default async function getCertificate(apiKey, id) {
const url = `https://api.zerossl.com/certificates/${id}?access_key=${apiKey}`;
const url = `https://api.zerossl.com/certificates/${id}`;

const requestOptions = {
method: 'GET',
headers: { },
};

const data = await requestApi(url, requestOptions);
const data = await requestApi(apiKey, url, requestOptions);

return new Certificate(data);
}
4 changes: 2 additions & 2 deletions packages/dashmate/src/ssl/zerossl/listCertificates.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default async function listCertificates(
page = 1,
search = undefined,
) {
let url = `https://api.zerossl.com/certificates?access_key=${apiKey}&limit=1000&page=${page}`;
let url = `https://api.zerossl.com/certificates?limit=1000&page=${page}`;

if (statuses.length > 0) {
url += `&statuses=${statuses.join(',')}`;
Expand All @@ -34,7 +34,7 @@ export default async function listCertificates(
headers: {},
};

const data = await requestApi(url, requestOptions);
const data = await requestApi(apiKey, url, requestOptions);

return data.results.map((certificateData) => new Certificate(certificateData));
}
86 changes: 80 additions & 6 deletions packages/dashmate/src/ssl/zerossl/requestApi.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,96 @@
import errorDescriptions from './errors/errorDescriptions.js';

const INVALID_API_KEY_MESSAGE = 'Invalid ZeroSSL API key';
const INVALID_API_RESPONSE_MESSAGE = 'Invalid ZeroSSL API response';
const REDACTED_VALUE = '[REDACTED]';

/**
* Redact the API key from a parsed ZeroSSL error without mutating the response.
*
* @param {*} value
* @param {string} apiKey
* @returns {*}
*/
function redactApiKey(value, apiKey) {
if (typeof value === 'string') {
return value.replaceAll(apiKey, REDACTED_VALUE);
}

if (Array.isArray(value)) {
return value.map((item) => redactApiKey(item, apiKey));
}

if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
redactApiKey(key, apiKey),
redactApiKey(item, apiKey),
]),
);
}

return value;
}

/**
* Build headers with one canonical ZeroSSL authorization value.
*
* @param {string} apiKey
* @param {HeadersInit} sourceHeaders
* @returns {Headers}
*/
function createHeaders(apiKey, sourceHeaders) {
if (typeof apiKey !== 'string' || apiKey.length === 0 || apiKey.trim() !== apiKey) {
throw new Error(INVALID_API_KEY_MESSAGE);
}

const authorization = `ApiKey ${apiKey}`;

try {
const headers = new Headers(sourceHeaders);
headers.set('Authorization', authorization);

if (headers.get('Authorization') !== authorization) {
throw new Error(INVALID_API_KEY_MESSAGE);
}

return headers;
} catch {
throw new Error(INVALID_API_KEY_MESSAGE);
}
}

/**
* Request the ZeroSSL API
*
* @param {string} apiKey
* @param {string} url
* @param {Object} options
* @returns {Promise<Object>}
*/
export default async function requestApi(url, options) {
const response = await fetch(url, options);
const data = await response.json();
export default async function requestApi(apiKey, url, options) {
const headers = createHeaders(apiKey, options.headers);
const requestOptions = {
...options,
headers,
};

const response = await fetch(url, requestOptions);

let data;
try {
data = await response.json();
} catch {
throw new Error(INVALID_API_RESPONSE_MESSAGE);
}

if (data.error) {
const errorMessage = errorDescriptions[data.error.code];
const sanitizedError = redactApiKey(data.error, apiKey);
const errorMessage = errorDescriptions[sanitizedError.code];

const error = new Error(errorMessage || data.error.type);
const error = new Error(errorMessage || sanitizedError.type);

Object.assign(error, data.error);
Object.assign(error, sanitizedError);

throw error;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/dashmate/src/ssl/zerossl/revokeCertificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default async function revokeCertificate(
apiKey,
id,
) {
const url = `https://api.zerossl.com/certificates/${id}/revoke?access_key=${apiKey}`;
const url = `https://api.zerossl.com/certificates/${id}/revoke`;

const requestOptions = {
method: 'POST',
Expand All @@ -21,5 +21,5 @@ export default async function revokeCertificate(
},
};

return requestApi(url, requestOptions);
return requestApi(apiKey, url, requestOptions);
}
4 changes: 2 additions & 2 deletions packages/dashmate/src/ssl/zerossl/verifyDomain.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function verifyDomain(id, apiKey) {
validation_method: 'HTTP_CSR_HASH',
});

const url = `https://api.zerossl.com/certificates/${id}/challenges?access_key=${apiKey}`;
const url = `https://api.zerossl.com/certificates/${id}/challenges`;

const requestOptions = {
method: 'POST',
Expand All @@ -24,5 +24,5 @@ export default async function verifyDomain(id, apiKey) {
},
};

return requestApi(url, requestOptions);
return requestApi(apiKey, url, requestOptions);
}
Loading
Loading