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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@ All notable changes to this project will be documented in this file.
For per-package release history, see the [GitHub Releases](https://github.com/multivmlabs/post-quantum-packages/releases) page.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- Implemented Phase 4 release hardening for `pq-algorithm-id/ts`, including cross-package compatibility tests.

### Changed

- Documented publish policy for `pq-algorithm-id`: bump exact pinned `pq-oid` dependency first, then release `pq-algorithm-id`, with no required cross-package release sequencing workflow.
- Clarified migration posture that `pq-oid` remains the low-level OID primitive layer while `pq-algorithm-id` is canonical for identifier mappings.
40 changes: 35 additions & 5 deletions packages/pq-algorithm-id/ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
fromJose,
fromOid,
fromX509AlgorithmIdentifier,
normalizeX509AlgorithmIdentifier,
resolveX509AlgorithmIdentifier,
toCose,
toJose,
toOid,
Expand All @@ -44,22 +46,40 @@ toX509AlgorithmIdentifier('ML-KEM-768');
// parameters: { kind: 'absent' }
// }

fromX509AlgorithmIdentifier({
normalizeX509AlgorithmIdentifier({
oid: '2.16.840.1.101.3.4.3.18',
parameters: null,
parameters: { kind: 'absent' },
});
// {
// oid: '2.16.840.1.101.3.4.3.18',
// parameters: { kind: 'null' }
// parameters: { kind: 'absent' }
// }

resolveX509AlgorithmIdentifier({
oid: '2.16.840.1.101.3.4.3.18',
parameters: { kind: 'absent' },
});
// {
// name: 'ML-DSA-65',
// oid: '2.16.840.1.101.3.4.3.18',
// parameters: { kind: 'absent' }
// }
```

## Behavior Notes

- `fromOid`, `fromJose`, and `fromCose` are strict lookups (no trimming, no case normalization).
- `toX509AlgorithmIdentifier(name)` emits `parameters: { kind: 'absent' }` by default.
- `fromX509AlgorithmIdentifier(input)` accepts `parameters` as `undefined`, `null`, `{ kind: 'absent' }`, or `{ kind: 'null' }`.
- `toOid`, `toJose`, and `toCose` require `name` to be a runtime string and throw `InvalidArgumentError` for non-string inputs.
- `toX509AlgorithmIdentifier(name)` emits `parameters: { kind: 'absent' }` and rejects unsupported `parametersEncoding` values with `InvalidArgumentError`; explicit `parametersEncoding: undefined` is treated the same as omission.
- `normalizeX509AlgorithmIdentifier(input)` treats both a missing `parameters` property and explicit `parameters: undefined` as absent.
- `normalizeX509AlgorithmIdentifier(input)` accepts only `null`, `{ kind: 'absent' }`, and `{ kind: 'null' }` for explicit parameter values, but strict X.509 policy only permits `parameters: { kind: 'absent' }`.
- `resolveX509AlgorithmIdentifier(input)` is an opt-in parser that returns `{ name, oid, parameters }` when you need the resolved algorithm name.
- `fromX509AlgorithmIdentifier(input)` is retained as a backwards-compatible alias for `normalizeX509AlgorithmIdentifier(input)`.
- X.509 APIs reject unknown own properties on `input`, `options`, and explicit `parameters` objects.
- X.509 APIs are designed for plain data objects; `Proxy` traps can execute during object inspection, and related inspection failures are surfaced as `InvalidArgumentError` with `cause`.
- Malformed API inputs (for example non-string `oid` or unexpected `parametersEncoding`) throw `InvalidArgumentError`.
- Unsupported mappings (for example `toJose('ML-KEM-512')`) throw typed errors.
- Diagnostic values in thrown messages are escaped and truncated to bounded previews to avoid oversized error payloads; raw metadata fields on typed errors are intentionally non-enumerable.

## Adoption Order

Expand All @@ -70,6 +90,16 @@ Suggested downstream migration order:
3. `pq-jws`
4. `pq-jwk`

## Publish Policy

Dependency and release policy for `0.x` series:

1. When `pq-algorithm-id` needs newer `pq-oid` behavior, first bump the exact pinned `pq-oid` dependency version in this package.
2. Then release a new `pq-algorithm-id` version.
3. No cross-package release sequencing workflow is required by this plan.

Until `1.0.0`, `pq-algorithm-id` uses an exact `pq-oid` version pin (exact semver, no range operators), and upstream dependency bumps are the trigger for `pq-algorithm-id` releases.

## License

MIT
13 changes: 12 additions & 1 deletion packages/pq-algorithm-id/ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -b",
"test": "bun test",
"prepublishOnly": "npm run build"
"test:node-dist": "node ./tests/node-dist-smoke.mjs",
"prepublishOnly": "npm test && npm run build && npm run test:node-dist"
},
"keywords": [
"post-quantum",
Expand All @@ -20,6 +28,9 @@
],
"author": "",
"license": "MIT",
"engines": {
"node": ">=18"
},
"dependencies": {
"pq-oid": "1.0.2"
},
Expand Down
71 changes: 55 additions & 16 deletions packages/pq-algorithm-id/ts/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,89 @@
import type { MappingTarget } from './types';
import type { MappingTarget } from './types.js';
import { describeUnknownValue } from './value-format.js';

export type AlgorithmIdentifierErrorCode =
| 'INVALID_ARGUMENT'
| 'REGISTRY_INVARIANT'
| 'UNKNOWN_ALGORITHM'
| 'UNKNOWN_IDENTIFIER'
| 'UNSUPPORTED_MAPPING';

interface ErrorOptionsLike {
readonly cause?: unknown;
}

function defineReadonlyNonEnumerableField(
target: object,
propertyName: string,
value: unknown,
): void {
Object.defineProperty(target, propertyName, {
value,
enumerable: false,
writable: false,
configurable: false,
});
}

export class AlgorithmIdentifierError extends Error {
readonly code: AlgorithmIdentifierErrorCode;

constructor(code: AlgorithmIdentifierErrorCode, message: string) {
super(message);
constructor(code: AlgorithmIdentifierErrorCode, message: string, options?: ErrorOptionsLike) {
super(message, options);
Object.setPrototypeOf(this, new.target.prototype);
this.name = new.target.name;
this.code = code;
}
}

export class UnknownAlgorithmError extends AlgorithmIdentifierError {
readonly algorithm: string;
readonly algorithm!: string;

constructor(algorithm: string) {
super('UNKNOWN_ALGORITHM', `Unknown algorithm '${algorithm}'.`);
this.algorithm = algorithm;
super('UNKNOWN_ALGORITHM', `Unknown algorithm '${describeUnknownValue(algorithm)}'.`);
defineReadonlyNonEnumerableField(this, 'algorithm', algorithm);
}
}

export class InvalidArgumentError extends AlgorithmIdentifierError {
readonly argumentName: string;

constructor(argumentName: string, message: string, options?: ErrorOptionsLike) {
super('INVALID_ARGUMENT', `Invalid argument '${argumentName}': ${message}`, options);
this.argumentName = argumentName;
}
}

export class RegistryInvariantError extends AlgorithmIdentifierError {
constructor(message: string, options?: ErrorOptionsLike) {
super('REGISTRY_INVARIANT', message, options);
}
}

export class UnknownIdentifierError extends AlgorithmIdentifierError {
readonly identifierType: MappingTarget;
readonly identifierValue: string | number;
readonly identifierType!: MappingTarget;
readonly identifierValue!: string | number;

constructor(identifierType: MappingTarget, identifierValue: string | number) {
super(
'UNKNOWN_IDENTIFIER',
`Unknown ${identifierType} identifier '${String(identifierValue)}'.`,
`Unknown ${identifierType} identifier '${describeUnknownValue(identifierValue)}'.`,
);
this.identifierType = identifierType;
this.identifierValue = identifierValue;
defineReadonlyNonEnumerableField(this, 'identifierType', identifierType);
defineReadonlyNonEnumerableField(this, 'identifierValue', identifierValue);
}
}

export class UnsupportedMappingError extends AlgorithmIdentifierError {
readonly mapping: MappingTarget;
readonly algorithm: string;
readonly mapping!: MappingTarget;
readonly algorithm!: string;

constructor(mapping: MappingTarget, algorithm: string) {
super('UNSUPPORTED_MAPPING', `Algorithm '${algorithm}' does not support ${mapping} mapping.`);
this.mapping = mapping;
this.algorithm = algorithm;
super(
'UNSUPPORTED_MAPPING',
`Algorithm '${describeUnknownValue(algorithm)}' does not support ${mapping} mapping.`,
);
defineReadonlyNonEnumerableField(this, 'mapping', mapping);
defineReadonlyNonEnumerableField(this, 'algorithm', algorithm);
}
}
16 changes: 11 additions & 5 deletions packages/pq-algorithm-id/ts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
export {
AlgorithmIdentifierError,
type AlgorithmIdentifierErrorCode,
InvalidArgumentError,
RegistryInvariantError,
UnknownAlgorithmError,
UnknownIdentifierError,
UnsupportedMappingError,
} from './errors';
export { fromCose, fromJose, fromOid, toCose, toJose, toOid } from './lookup';
} from './errors.js';
export { fromCose, fromJose, fromOid, toCose, toJose, toOid } from './lookup.js';
export {
deriveOidFromName,
getIdentifierRecord,
listIdentifierRecords,
listRegistryAlgorithmNames,
} from './registry';
} from './registry.js';
export type {
AlgorithmName,
CoseIdentifier,
IdentifierRecord,
IdentifierRecordMap,
JoseIdentifier,
MappingTarget,
X509ParametersEncoding,
X509ParametersPolicy,
} from './types';
} from './types.js';
export {
fromX509AlgorithmIdentifier,
normalizeX509AlgorithmIdentifier,
resolveX509AlgorithmIdentifier,
toX509AlgorithmIdentifier,
type ResolvedX509AlgorithmIdentifier,
type X509AlgorithmIdentifier,
type X509AlgorithmIdentifierInput,
type X509AlgorithmIdentifierOptions,
type X509NormalizedParameters,
} from './x509';
} from './x509.js';
Loading
Loading