Skip to content

Commit 7712f26

Browse files
authored
chore(pq-algorithm-id/ts): phase 4 - release hardening and adoption validation (ENG-1917) (#29)
## Summary <!-- What does this PR do? Keep it brief. --> ## Package(s) <!-- Which package(s) are modified? --> ## Languages - [ ] TypeScript - [ ] Rust ## Checklist - [ ] Tests pass for all modified packages - [ ] Linting/formatting passes (`biome check`, `cargo fmt`) - [ ] Both language implementations are consistent (or noted as follow-up) - [ ] Package README updated if public API changed - [ ] No unnecessary dependencies added ## Related Issues <!-- Link any related issues: Fixes #123, Closes #456 -->
1 parent c734ff4 commit 7712f26

21 files changed

Lines changed: 1374 additions & 215 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,14 @@ All notable changes to this project will be documented in this file.
55
For per-package release history, see the [GitHub Releases](https://github.com/multivmlabs/post-quantum-packages/releases) page.
66

77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8+
9+
## [Unreleased]
10+
11+
### Added
12+
13+
- Implemented Phase 4 release hardening for `pq-algorithm-id/ts`, including cross-package compatibility tests.
14+
15+
### Changed
16+
17+
- 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.
18+
- Clarified migration posture that `pq-oid` remains the low-level OID primitive layer while `pq-algorithm-id` is canonical for identifier mappings.

packages/pq-algorithm-id/ts/README.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
fromJose,
2222
fromOid,
2323
fromX509AlgorithmIdentifier,
24+
normalizeX509AlgorithmIdentifier,
25+
resolveX509AlgorithmIdentifier,
2426
toCose,
2527
toJose,
2628
toOid,
@@ -44,22 +46,40 @@ toX509AlgorithmIdentifier('ML-KEM-768');
4446
// parameters: { kind: 'absent' }
4547
// }
4648

47-
fromX509AlgorithmIdentifier({
49+
normalizeX509AlgorithmIdentifier({
4850
oid: '2.16.840.1.101.3.4.3.18',
49-
parameters: null,
51+
parameters: { kind: 'absent' },
5052
});
5153
// {
5254
// oid: '2.16.840.1.101.3.4.3.18',
53-
// parameters: { kind: 'null' }
55+
// parameters: { kind: 'absent' }
56+
// }
57+
58+
resolveX509AlgorithmIdentifier({
59+
oid: '2.16.840.1.101.3.4.3.18',
60+
parameters: { kind: 'absent' },
61+
});
62+
// {
63+
// name: 'ML-DSA-65',
64+
// oid: '2.16.840.1.101.3.4.3.18',
65+
// parameters: { kind: 'absent' }
5466
// }
5567
```
5668

5769
## Behavior Notes
5870

5971
- `fromOid`, `fromJose`, and `fromCose` are strict lookups (no trimming, no case normalization).
60-
- `toX509AlgorithmIdentifier(name)` emits `parameters: { kind: 'absent' }` by default.
61-
- `fromX509AlgorithmIdentifier(input)` accepts `parameters` as `undefined`, `null`, `{ kind: 'absent' }`, or `{ kind: 'null' }`.
72+
- `toOid`, `toJose`, and `toCose` require `name` to be a runtime string and throw `InvalidArgumentError` for non-string inputs.
73+
- `toX509AlgorithmIdentifier(name)` emits `parameters: { kind: 'absent' }` and rejects unsupported `parametersEncoding` values with `InvalidArgumentError`; explicit `parametersEncoding: undefined` is treated the same as omission.
74+
- `normalizeX509AlgorithmIdentifier(input)` treats both a missing `parameters` property and explicit `parameters: undefined` as absent.
75+
- `normalizeX509AlgorithmIdentifier(input)` accepts only `null`, `{ kind: 'absent' }`, and `{ kind: 'null' }` for explicit parameter values, but strict X.509 policy only permits `parameters: { kind: 'absent' }`.
76+
- `resolveX509AlgorithmIdentifier(input)` is an opt-in parser that returns `{ name, oid, parameters }` when you need the resolved algorithm name.
77+
- `fromX509AlgorithmIdentifier(input)` is retained as a backwards-compatible alias for `normalizeX509AlgorithmIdentifier(input)`.
78+
- X.509 APIs reject unknown own properties on `input`, `options`, and explicit `parameters` objects.
79+
- 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`.
80+
- Malformed API inputs (for example non-string `oid` or unexpected `parametersEncoding`) throw `InvalidArgumentError`.
6281
- Unsupported mappings (for example `toJose('ML-KEM-512')`) throw typed errors.
82+
- 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.
6383

6484
## Adoption Order
6585

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

93+
## Publish Policy
94+
95+
Dependency and release policy for `0.x` series:
96+
97+
1. When `pq-algorithm-id` needs newer `pq-oid` behavior, first bump the exact pinned `pq-oid` dependency version in this package.
98+
2. Then release a new `pq-algorithm-id` version.
99+
3. No cross-package release sequencing workflow is required by this plan.
100+
101+
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.
102+
73103
## License
74104

75105
MIT

packages/pq-algorithm-id/ts/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@
55
"type": "module",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"import": "./dist/index.js",
12+
"default": "./dist/index.js"
13+
}
14+
},
815
"files": [
916
"dist"
1017
],
1118
"scripts": {
1219
"build": "tsc -b",
1320
"test": "bun test",
14-
"prepublishOnly": "npm run build"
21+
"test:node-dist": "node ./tests/node-dist-smoke.mjs",
22+
"prepublishOnly": "npm test && npm run build && npm run test:node-dist"
1523
},
1624
"keywords": [
1725
"post-quantum",
@@ -20,6 +28,9 @@
2028
],
2129
"author": "",
2230
"license": "MIT",
31+
"engines": {
32+
"node": ">=18"
33+
},
2334
"dependencies": {
2435
"pq-oid": "1.0.2"
2536
},
Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,89 @@
1-
import type { MappingTarget } from './types';
1+
import type { MappingTarget } from './types.js';
2+
import { describeUnknownValue } from './value-format.js';
23

34
export type AlgorithmIdentifierErrorCode =
5+
| 'INVALID_ARGUMENT'
6+
| 'REGISTRY_INVARIANT'
47
| 'UNKNOWN_ALGORITHM'
58
| 'UNKNOWN_IDENTIFIER'
69
| 'UNSUPPORTED_MAPPING';
710

11+
interface ErrorOptionsLike {
12+
readonly cause?: unknown;
13+
}
14+
15+
function defineReadonlyNonEnumerableField(
16+
target: object,
17+
propertyName: string,
18+
value: unknown,
19+
): void {
20+
Object.defineProperty(target, propertyName, {
21+
value,
22+
enumerable: false,
23+
writable: false,
24+
configurable: false,
25+
});
26+
}
27+
828
export class AlgorithmIdentifierError extends Error {
929
readonly code: AlgorithmIdentifierErrorCode;
1030

11-
constructor(code: AlgorithmIdentifierErrorCode, message: string) {
12-
super(message);
31+
constructor(code: AlgorithmIdentifierErrorCode, message: string, options?: ErrorOptionsLike) {
32+
super(message, options);
33+
Object.setPrototypeOf(this, new.target.prototype);
1334
this.name = new.target.name;
1435
this.code = code;
1536
}
1637
}
1738

1839
export class UnknownAlgorithmError extends AlgorithmIdentifierError {
19-
readonly algorithm: string;
40+
readonly algorithm!: string;
2041

2142
constructor(algorithm: string) {
22-
super('UNKNOWN_ALGORITHM', `Unknown algorithm '${algorithm}'.`);
23-
this.algorithm = algorithm;
43+
super('UNKNOWN_ALGORITHM', `Unknown algorithm '${describeUnknownValue(algorithm)}'.`);
44+
defineReadonlyNonEnumerableField(this, 'algorithm', algorithm);
45+
}
46+
}
47+
48+
export class InvalidArgumentError extends AlgorithmIdentifierError {
49+
readonly argumentName: string;
50+
51+
constructor(argumentName: string, message: string, options?: ErrorOptionsLike) {
52+
super('INVALID_ARGUMENT', `Invalid argument '${argumentName}': ${message}`, options);
53+
this.argumentName = argumentName;
54+
}
55+
}
56+
57+
export class RegistryInvariantError extends AlgorithmIdentifierError {
58+
constructor(message: string, options?: ErrorOptionsLike) {
59+
super('REGISTRY_INVARIANT', message, options);
2460
}
2561
}
2662

2763
export class UnknownIdentifierError extends AlgorithmIdentifierError {
28-
readonly identifierType: MappingTarget;
29-
readonly identifierValue: string | number;
64+
readonly identifierType!: MappingTarget;
65+
readonly identifierValue!: string | number;
3066

3167
constructor(identifierType: MappingTarget, identifierValue: string | number) {
3268
super(
3369
'UNKNOWN_IDENTIFIER',
34-
`Unknown ${identifierType} identifier '${String(identifierValue)}'.`,
70+
`Unknown ${identifierType} identifier '${describeUnknownValue(identifierValue)}'.`,
3571
);
36-
this.identifierType = identifierType;
37-
this.identifierValue = identifierValue;
72+
defineReadonlyNonEnumerableField(this, 'identifierType', identifierType);
73+
defineReadonlyNonEnumerableField(this, 'identifierValue', identifierValue);
3874
}
3975
}
4076

4177
export class UnsupportedMappingError extends AlgorithmIdentifierError {
42-
readonly mapping: MappingTarget;
43-
readonly algorithm: string;
78+
readonly mapping!: MappingTarget;
79+
readonly algorithm!: string;
4480

4581
constructor(mapping: MappingTarget, algorithm: string) {
46-
super('UNSUPPORTED_MAPPING', `Algorithm '${algorithm}' does not support ${mapping} mapping.`);
47-
this.mapping = mapping;
48-
this.algorithm = algorithm;
82+
super(
83+
'UNSUPPORTED_MAPPING',
84+
`Algorithm '${describeUnknownValue(algorithm)}' does not support ${mapping} mapping.`,
85+
);
86+
defineReadonlyNonEnumerableField(this, 'mapping', mapping);
87+
defineReadonlyNonEnumerableField(this, 'algorithm', algorithm);
4988
}
5089
}
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
export {
22
AlgorithmIdentifierError,
33
type AlgorithmIdentifierErrorCode,
4+
InvalidArgumentError,
5+
RegistryInvariantError,
46
UnknownAlgorithmError,
57
UnknownIdentifierError,
68
UnsupportedMappingError,
7-
} from './errors';
8-
export { fromCose, fromJose, fromOid, toCose, toJose, toOid } from './lookup';
9+
} from './errors.js';
10+
export { fromCose, fromJose, fromOid, toCose, toJose, toOid } from './lookup.js';
911
export {
1012
deriveOidFromName,
1113
getIdentifierRecord,
1214
listIdentifierRecords,
1315
listRegistryAlgorithmNames,
14-
} from './registry';
16+
} from './registry.js';
1517
export type {
18+
AlgorithmName,
1619
CoseIdentifier,
1720
IdentifierRecord,
1821
IdentifierRecordMap,
1922
JoseIdentifier,
2023
MappingTarget,
2124
X509ParametersEncoding,
2225
X509ParametersPolicy,
23-
} from './types';
26+
} from './types.js';
2427
export {
2528
fromX509AlgorithmIdentifier,
29+
normalizeX509AlgorithmIdentifier,
30+
resolveX509AlgorithmIdentifier,
2631
toX509AlgorithmIdentifier,
32+
type ResolvedX509AlgorithmIdentifier,
2733
type X509AlgorithmIdentifier,
2834
type X509AlgorithmIdentifierInput,
2935
type X509AlgorithmIdentifierOptions,
3036
type X509NormalizedParameters,
31-
} from './x509';
37+
} from './x509.js';

0 commit comments

Comments
 (0)