Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
67 changes: 41 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,21 @@ and structured `details`.

```tree
micro509/
├── src/ # domain barrels + public modules
├── src/ # public entrypoints (src/*.ts) + domain implementation
│ ├── index.ts # micro509
│ ├── der.ts # micro509/der
│ ├── keys.ts # micro509/keys
│ ├── pem.ts # micro509/pem
│ ├── pkcs.ts # micro509/pkcs
│ ├── result.ts # micro509/result
│ ├── revocation.ts # micro509/revocation
│ ├── verify.ts # micro509/verify
│ ├── x509.ts # micro509/x509
│ ├── x509/ # cert + CSR + extension + parse APIs
│ ├── verify/ # chain validation + policy + identity checks
│ ├── revocation/ # CRL/OCSP lifecycles
│ ├── keys/ # key import/export and generation
│ ├── der/ # public DER encode/decode surface (micro509/der)
│ ├── der/ # public DER encode/decode surface
│ ├── pem/ # PEM encode/decode boundary
│ ├── pkcs/ # PKCS-7 and PKCS#12 workflows
│ ├── result/ # shared result/error algebra
Expand All @@ -68,26 +77,28 @@ micro509/

## WHERE TO LOOK

| Task | Location | Notes |
| --------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------- |
| Public API surface | `src/index.ts` | root barrel for `micro509` |
| Domain entrypoints | `src/x509/`, `src/verify/`, `src/revocation/`, `src/keys/` | domain-specific high-level entry surfaces |
| Package entry routing | `package.json` | `exports`, `imports`, scripts |
| Chain validation | `src/verify/verify.ts` | candidate path building, policy composition |
| Certificate/CSR parsing | `src/x509/parse.ts` | DER/PEM parse boundary + extension decoding |
| Revocation | `src/revocation/crl.ts`, `src/revocation/ocsp.ts` | CRL + OCSP creation, parse, validate, verify |
| Key import/export | `src/keys/keys.ts` | PKCS#1/8, SEC1, SPKI, JWK flows |
| DER codec surface | `src/der/der.ts` | public DER encode/decode (`micro509/der`) |
| Extension model/builders | `src/x509/extensions.ts` | typed extension schema and encoder helpers |
| Test helpers and internals probes | `test/helpers.ts`, `test/internals.test.ts` | shared DER helpers, internal probing through imports |
| Standards scope | `docs/PKIX-SCOPE.md` | claim boundaries |
| Docs site | `site/.vitepress/config.ts`, `site/guide/`, `site/api/` | VitePress config, authored guides, generated API |
| Reusable CI actions | `.github/actions/` | shared setup + release version validation |
| Task | Location | Notes |
| --------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------- |
| Public API surface | `src/index.ts` | root barrel for `micro509` |
| Domain entrypoints | `src/x509.ts`, `src/verify.ts`, `src/revocation.ts`, `src/keys.ts` | domain-specific high-level entry surfaces |
| Package entry routing | `package.json`, `tsdown.config.ts` | `exports` and `jsr.json` are derived from `src/*.ts` |
| Chain validation | `src/verify/verify.ts` | candidate path building, policy composition |
| Certificate/CSR parsing | `src/x509/parse.ts` | DER/PEM parse boundary + extension decoding |
| Revocation | `src/revocation/crl.ts`, `src/revocation/ocsp.ts` | CRL + OCSP creation, parse, validate, verify |
| Key import/export | `src/keys/keys.ts` | PKCS#1/8, SEC1, SPKI, JWK flows |
| DER codec surface | `src/der/der.ts` | public DER encode/decode (`micro509/der`) |
| Extension model/builders | `src/x509/extensions.ts` | typed extension schema and encoder helpers |
| Test helpers and internals probes | `test/helpers.ts`, `test/internals.test.ts` | shared DER helpers, internal probing through imports |
| Standards scope | `docs/PKIX-SCOPE.md` | claim boundaries |
| Docs site | `site/.vitepress/config.ts`, `site/guide/`, `site/api/` | VitePress config, authored guides, generated API |
| Reusable CI actions | `.github/actions/` | shared setup + release version validation |

## CONVENTIONS

- Domain entrypoints own feature ownership; concrete lifecycle modules do implementation.
- `src/*/` barrels are re-export-only unless local file owners expand naturally.
- Root `src/*.ts` files are the package entrypoints and are re-export-only; tsdown
globs them, so a new root file publishes a new `micro509/<name>` subpath. Domain
implementation goes in `src/<domain>/`, internals in `src/internal/`.
- Relative imports use `.ts` extensions; `#micro509/*` subpath imports are extensionless (the `imports` map supplies `.ts`).
- Import boundaries: public leaf modules may use `#micro509/internal/*`, not sibling barrels.
- Return typed result unions for expected failures; throw only for invariants.
Expand All @@ -112,14 +123,18 @@ Avoid the following in this project:

## CODE MAP

| Symbol | Type | Location | Refs |
| ------------------------- | ------------- | ------------------- | ----------------------------------------- |
| `src/index.ts` | barrel export | root surface | Re-exports all stable API slices |
| `src/x509/index.ts` | domain barrel | X.509 feature slice | Certificate, CSR, parse, extension APIs |
| `src/verify/index.ts` | domain barrel | verification slice | Path, policy, identity, name constraints |
| `src/revocation/index.ts` | domain barrel | revocation slice | CRL/OCSP orchestration |
| `src/der/index.ts` | domain barrel | DER slice | public DER encode/decode surface |
| `src/result/result.ts` | result ADT | shared model | central `Result`/`Micro509Error` contract |
| Symbol | Type | Location | Refs |
| ---------------------- | ------------- | ------------------- | ----------------------------------------- |
| `src/index.ts` | barrel export | root surface | Re-exports all stable API slices |
| `src/der.ts` | domain barrel | DER slice | public DER encode/decode surface |
| `src/keys.ts` | domain barrel | key slice | import/export, generation, encryption |
| `src/pem.ts` | domain barrel | PEM slice | encode/decode and block classification |
| `src/pkcs.ts` | domain barrel | PKCS slice | PKCS-7 and PKCS#12 workflows |
| `src/result.ts` | domain barrel | result slice | `Result` and error constructors |
| `src/revocation.ts` | domain barrel | revocation slice | CRL/OCSP orchestration |
| `src/verify.ts` | domain barrel | verification slice | Path, policy, identity, name constraints |
| `src/x509.ts` | domain barrel | X.509 feature slice | Certificate, CSR, parse, extension APIs |
| `src/result/result.ts` | result ADT | shared model | central `Result`/`Micro509Error` contract |

## COMMANDS

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Public source entrypoints are flat `src/*.ts` files: `src/x509/index.ts`
becomes `src/x509.ts`, and the eight nested barrels are gone. `tsdown.config.ts`
globs `entry: ['src/*.ts']`, so `package.json#exports` and `jsr.json#exports`
derive from the filesystem rather than a hand-maintained entry map, and
`package.json#imports` collapses onto the `#micro509/*` wildcard. Package
specifiers (`micro509`, `micro509/x509`, …) and every exported symbol are
unchanged; JSR consumers reading source paths see the new flat layout.
- Builder input-validation now throws a `ResultError` carrying a stable
machine-readable `code` rather than a bare `Error`. `createCertificate`, the
`encode*` extension helpers, distinguished-name encoding, and CRL/IDP encoding
Expand Down
16 changes: 8 additions & 8 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"license": "MIT",
"exports": {
".": "./src/index.ts",
"./der": "./src/der/index.ts",
"./keys": "./src/keys/index.ts",
"./pem": "./src/pem/index.ts",
"./pkcs": "./src/pkcs/index.ts",
"./result": "./src/result/index.ts",
"./revocation": "./src/revocation/index.ts",
"./verify": "./src/verify/index.ts",
"./x509": "./src/x509/index.ts"
"./der": "./src/der.ts",
"./keys": "./src/keys.ts",
"./pem": "./src/pem.ts",
"./pkcs": "./src/pkcs.ts",
"./result": "./src/result.ts",
"./revocation": "./src/revocation.ts",
"./verify": "./src/verify.ts",
"./x509": "./src/x509.ts"
},
"publish": {
"include": ["src/**/*.ts", "src/**/*.json", "package.json", "LICENSE", "README.md"],
Expand Down
8 changes: 0 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@
"imports": {
"#micro509": "./src/index.ts",
"#micro509/*": "./src/*.ts",
"#micro509/der": "./src/der/index.ts",
"#micro509/keys": "./src/keys/index.ts",
"#micro509/pem": "./src/pem/index.ts",
"#micro509/pkcs": "./src/pkcs/index.ts",
"#micro509/result": "./src/result/index.ts",
"#micro509/revocation": "./src/revocation/index.ts",
"#micro509/verify": "./src/verify/index.ts",
"#micro509/x509": "./src/x509/index.ts",
"#micro509/internal/*": "./src/internal/*.ts",
"#pkg": "./package.json",
"#jsr": "./jsr.json",
Expand Down
30 changes: 16 additions & 14 deletions src/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
# `src/` - Module Map

Public domain entrypoints are one-level buckets, implementation details stay in
`src/internal/`.
Public entrypoints are the root `src/*.ts` files; domain implementation stays in
`src/<domain>/` and internals in `src/internal/`.

## OVERVIEW

`src/` is the library boundary. High-level workflow ownership is in domain
barrels under `x509`, `verify`, `revocation`, `keys`, `pem`, `pkcs`, and
`result`.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Can you be any more verbose... wtf...

`src/` is the library boundary. Each root `src/<domain>.ts` file is a package
entrypoint that re-exports its domain; implementation lives in the matching
`src/<domain>/` directory.

## WHERE TO LOOK

| Area | File/dir | Notes |
| ------------------ | --------------------------------- | --------------------------------------------------- |
| Package root | `index.ts` | all stable package exports |
| Domain entrypoints | `x509/`, `verify/`, `revocation/` | re-export-only in most cases |
| Key APIs | `keys/` | import/export, generation, encryption options |
| PEM boundary | `pem/` | encode/decode and block classification |
| PKCS workflows | `pkcs/` | PFX and PKCS#7 data lifecycles |
| Result model | `result/` | shared typed `Result` and error constructors |
| Internal spine | `internal/` | ASN.1, crypto, shared helpers, verification engines |
| Area | File/dir | Notes |
| ------------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| Package root | `index.ts` | all stable package exports |
| Domain entrypoints | `der.ts`, `keys.ts`, `pem.ts`, `pkcs.ts`, `result.ts`, `revocation.ts`, `verify.ts`, `x509.ts` | re-export-only |

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

no need to enumerate all in a fucking table cell

| Key APIs | `keys/` | import/export, generation, encryption options |
| PEM boundary | `pem/` | encode/decode and block classification |
| PKCS workflows | `pkcs/` | PFX and PKCS#7 data lifecycles |
| Result model | `result/` | shared typed `Result` and error constructors |
| Internal spine | `internal/` | ASN.1, crypto, shared helpers, verification engines |

## LOCAL CONVENTIONS

- Keep APIs by domain barrel; add files inside existing domain unless the domain
model clearly needs a new public ownership file.
- Root `src/*.ts` is reserved for package entrypoints. tsdown globs it, so a new
root file publishes `micro509/<name>`; `test/conventions.test.ts` pins the set.
- New OIDs go in `src/internal/asn1/oids.json` under their registration arc and
are consumed as `OIDS.<name>`; never inline a dotted-decimal literal in
source.
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/internal/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Non-public internals for codecs, cryptography, and verification engines.
## OVERVIEW

`src/internal/` hosts reusable low-level spines consumed by all domains.
Public barrels should stay in domain folders.
Public barrels are the root `src/*.ts` files and never live here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

NO SHIT, THE DIRECTORY IS CALLED INTERNAL.


## STRUCTURE

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
26 changes: 25 additions & 1 deletion test/conventions.test.ts

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

get rid of this file

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { Glob } from 'bun';
import { VERIFY_ERROR_CODES } from '#micro509/verify';
import { projectRoot, srcRoot } from '#test/helpers';

/** Stems of the root `src/*.ts` files, which tsdown publishes as package subpaths. */
function rootEntryStems(): readonly string[] {
return [...new Glob('*.ts').scanSync({ cwd: srcRoot })]
.map((name) => name.slice(0, -'.ts'.length))
.sort();
}

function sourceFiles(): readonly string[] {
return [...new Glob('**/*.ts').scanSync({ cwd: srcRoot, absolute: true })];
}
Expand Down Expand Up @@ -94,14 +101,31 @@ describe('repo conventions (AGENTS.md / CONTRIBUTING.md)', () => {
const allOrThrow = new Set<string>();
for (const [domain, names] of orThrowByDomain) {
if (names.size === 0) continue;
offenders.push(...(await missingOrThrowExports(`${domain}/index.ts`, names)));
offenders.push(...(await missingOrThrowExports(`${domain}.ts`, names)));
for (const name of names) allOrThrow.add(name);
}
offenders.push(...(await missingOrThrowExports('index.ts', allOrThrow)));

expect(offenders).toEqual([]);
});

it('publishes exactly the intended root entrypoints', () => {
// tsdown globs src/*.ts, so a stray root file silently becomes a published
// subpath. Domain implementation belongs in src/<domain>/, internals in
// src/internal/.
expect(rootEntryStems()).toEqual([
'der',
'index',
'keys',
'pem',
'pkcs',
'result',
'revocation',
'verify',
'x509',
]);
});

it('site error-code table matches VERIFY_ERROR_CODES exactly', async () => {
const guide = await Bun.file(`${projectRoot}/site/guide/verification.md`).text();
// First backtick-wrapped snake_case token of each table row
Expand Down
33 changes: 11 additions & 22 deletions tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
import { existsSync } from 'node:fs';
import { existsSync, readdirSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import type { UserConfig } from 'tsdown';
import { defineConfig } from 'tsdown';
import jsr from '#jsr' with { type: 'json' };
import pkg from '#pkg' with { type: 'json' };

export const entries = {
index: 'src/index.ts',
der: 'src/der/index.ts',
keys: 'src/keys/index.ts',
pem: 'src/pem/index.ts',
pkcs: 'src/pkcs/index.ts',
result: 'src/result/index.ts',
revocation: 'src/revocation/index.ts',
verify: 'src/verify/index.ts',
x509: 'src/x509/index.ts',
} satisfies UserConfig['entry'];

export default defineConfig((options) => ({
entry: entries,
entry: ['src/*.ts'],
name: pkg.name,
format: 'esm',
dts: true,
Expand Down Expand Up @@ -60,14 +47,16 @@ export default defineConfig((options) => ({
onSuccess: 'dprint fmt {jsr,package}.json',
hooks: {
'build:done': async () => {
const jsrNext = { ...jsr, exports: {} };
jsrNext.exports = Object.fromEntries(
Object.entries(entries).map(([name, sourcePath]) => [
name === 'index' ? '.' : `./${name}`,
`./${sourcePath}`,
]),
const exports = Object.fromEntries(
readdirSync('src')
.filter((file) => file.endsWith('.ts'))
.map((file) => {
const name = file.slice(0, -'.ts'.length);
return [name === 'index' ? '.' : `./${name}`, `./src/${file}`];
})
.sort(),
);
jsrNext.version = pkg.version;
const jsrNext = { ...jsr, exports, version: pkg.version };
await writeFile('jsr.json', `${JSON.stringify(jsrNext, null, '\t')}\n`);
},
},
Expand Down
Loading