mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): cut alpha.1 candidate milestone
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qinglong/runtime-core",
|
||||
"version": "3.0.0-alpha.0",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"private": true,
|
||||
"description": "QingLong 3.0 profile-neutral runtime contracts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -961,6 +961,16 @@
|
||||
"require": "./dist/security/identity-credential/apiCredentialToken.js",
|
||||
"default": "./dist/security/identity-credential/apiCredentialToken.js"
|
||||
},
|
||||
"./api-credential-pepper-keyring": {
|
||||
"types": "./dist/security/identity-credential/apiCredentialPepperKeyring.d.ts",
|
||||
"require": "./dist/security/identity-credential/apiCredentialPepperKeyring.js",
|
||||
"default": "./dist/security/identity-credential/apiCredentialPepperKeyring.js"
|
||||
},
|
||||
"./api-credential-pepper-reference": {
|
||||
"types": "./dist/security/identity-credential/apiCredentialPepperReference.d.ts",
|
||||
"require": "./dist/security/identity-credential/apiCredentialPepperReference.js",
|
||||
"default": "./dist/security/identity-credential/apiCredentialPepperReference.js"
|
||||
},
|
||||
"./identity-administration": {
|
||||
"types": "./dist/security/identity-credential/identityAdministration.d.ts",
|
||||
"require": "./dist/security/identity-credential/identityAdministration.js",
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { assertApiCredentialPepperKeyId } from './apiCredential';
|
||||
import { assertApiCredentialPepper } from './apiCredentialToken';
|
||||
|
||||
export const MAX_API_CREDENTIAL_PEPPER_KEYS = 2;
|
||||
|
||||
export interface ApiCredentialPepperKey {
|
||||
readonly pepperKeyId: string;
|
||||
readonly pepper: string;
|
||||
}
|
||||
|
||||
export interface ApiCredentialPepperKeyring {
|
||||
readonly schemaVersion: 1;
|
||||
readonly activePepperKeyId: string;
|
||||
readonly keys: readonly Readonly<ApiCredentialPepperKey>[];
|
||||
}
|
||||
|
||||
export class ApiCredentialPepperKeyringConfigurationError extends TypeError {
|
||||
constructor(message: string) {
|
||||
super(`API credential pepper keyring is invalid: ${message}`);
|
||||
this.name = 'ApiCredentialPepperKeyringConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
`${label} must be an object`,
|
||||
);
|
||||
}
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...expectedKeys].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
`${label} shape is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKey(value: unknown): Readonly<ApiCredentialPepperKey> {
|
||||
exactObject(value, ['pepper', 'pepperKeyId'], 'key');
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(value.pepperKeyId as string);
|
||||
assertApiCredentialPepper(value.pepper as string);
|
||||
} catch {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
'key material is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
pepperKeyId: value.pepperKeyId as string,
|
||||
pepper: value.pepper as string,
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeApiCredentialPepperKeyring(
|
||||
value: unknown,
|
||||
): Readonly<ApiCredentialPepperKeyring> {
|
||||
exactObject(
|
||||
value,
|
||||
['activePepperKeyId', 'keys', 'schemaVersion'],
|
||||
'keyring',
|
||||
);
|
||||
if (
|
||||
value.schemaVersion !== 1 ||
|
||||
!Array.isArray(value.keys) ||
|
||||
value.keys.length < 1 ||
|
||||
value.keys.length > MAX_API_CREDENTIAL_PEPPER_KEYS
|
||||
) {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
'version or key count is invalid',
|
||||
);
|
||||
}
|
||||
let activePepperKeyId: string;
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(value.activePepperKeyId as string);
|
||||
activePepperKeyId = value.activePepperKeyId as string;
|
||||
} catch {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
'activePepperKeyId is invalid',
|
||||
);
|
||||
}
|
||||
const keys = value.keys.map(normalizeKey);
|
||||
const keyIds = new Set(keys.map((key) => key.pepperKeyId));
|
||||
if (keyIds.size !== keys.length || !keyIds.has(activePepperKeyId)) {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
'keys must be unique and contain the active key',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
activePepperKeyId,
|
||||
keys: Object.freeze(keys),
|
||||
});
|
||||
}
|
||||
|
||||
export function createSingletonApiCredentialPepperKeyring(
|
||||
pepper: string,
|
||||
pepperKeyId: string,
|
||||
): Readonly<ApiCredentialPepperKeyring> {
|
||||
return normalizeApiCredentialPepperKeyring({
|
||||
schemaVersion: 1,
|
||||
activePepperKeyId: pepperKeyId,
|
||||
keys: [{ pepperKeyId, pepper }],
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveApiCredentialPepperKey(
|
||||
keyring: Readonly<ApiCredentialPepperKeyring>,
|
||||
pepperKeyId: string,
|
||||
): Readonly<ApiCredentialPepperKey> | null {
|
||||
const normalized = normalizeApiCredentialPepperKeyring(keyring);
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(pepperKeyId);
|
||||
} catch {
|
||||
throw new ApiCredentialPepperKeyringConfigurationError(
|
||||
'pepperKeyId is invalid',
|
||||
);
|
||||
}
|
||||
return (
|
||||
normalized.keys.find((key) => key.pepperKeyId === pepperKeyId) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function activeApiCredentialPepperKey(
|
||||
keyring: Readonly<ApiCredentialPepperKeyring>,
|
||||
): Readonly<ApiCredentialPepperKey> {
|
||||
const normalized = normalizeApiCredentialPepperKeyring(keyring);
|
||||
return resolveApiCredentialPepperKey(
|
||||
normalized,
|
||||
normalized.activePepperKeyId,
|
||||
)!;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { assertApiCredentialPepperKeyId } from './apiCredential';
|
||||
|
||||
export const MAX_API_CREDENTIAL_PEPPER_REFERENCES = 64;
|
||||
|
||||
export interface ApiCredentialPepperReferenceInspection {
|
||||
readonly pepperKeyId: string;
|
||||
readonly observedAtMs: number;
|
||||
readonly credentialIds: readonly string[];
|
||||
readonly hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface ApiCredentialPepperReferenceRepository {
|
||||
inspect(
|
||||
pepperKeyId: string,
|
||||
limit?: number,
|
||||
): Promise<Readonly<ApiCredentialPepperReferenceInspection>>;
|
||||
}
|
||||
|
||||
export class ApiCredentialPepperReferenceUnavailableError extends Error {
|
||||
readonly code = 'API_CREDENTIAL_PEPPER_REFERENCE_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('API credential pepper references are unavailable');
|
||||
this.name = 'ApiCredentialPepperReferenceUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeApiCredentialPepperReferenceLimit(
|
||||
value: number | undefined,
|
||||
): number {
|
||||
const resolved = value ?? MAX_API_CREDENTIAL_PEPPER_REFERENCES;
|
||||
if (
|
||||
!Number.isSafeInteger(resolved) ||
|
||||
resolved < 1 ||
|
||||
resolved > MAX_API_CREDENTIAL_PEPPER_REFERENCES
|
||||
) {
|
||||
throw new TypeError('API credential pepper reference limit is invalid');
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function normalizeApiCredentialPepperReferenceKeyId(
|
||||
value: string,
|
||||
): string {
|
||||
assertApiCredentialPepperKeyId(value);
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
activeApiCredentialPepperKey,
|
||||
createSingletonApiCredentialPepperKeyring,
|
||||
normalizeApiCredentialPepperKeyring,
|
||||
resolveApiCredentialPepperKey,
|
||||
} = require('../dist/security/identity-credential/apiCredentialPepperKeyring.js');
|
||||
|
||||
const LEGACY = 'A'.repeat(43);
|
||||
const NEXT = Buffer.alloc(32, 2).toString('base64url');
|
||||
|
||||
test('normalizes a bounded dual-generation keyring and resolves exact keys', () => {
|
||||
const keyring = normalizeApiCredentialPepperKeyring({
|
||||
schemaVersion: 1,
|
||||
activePepperKeyId: 'rotation-2026-08',
|
||||
keys: [
|
||||
{ pepperKeyId: 'legacy-v1', pepper: LEGACY },
|
||||
{ pepperKeyId: 'rotation-2026-08', pepper: NEXT },
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(activeApiCredentialPepperKey(keyring), {
|
||||
pepperKeyId: 'rotation-2026-08',
|
||||
pepper: NEXT,
|
||||
});
|
||||
assert.deepEqual(resolveApiCredentialPepperKey(keyring, 'legacy-v1'), {
|
||||
pepperKeyId: 'legacy-v1',
|
||||
pepper: LEGACY,
|
||||
});
|
||||
assert.equal(resolveApiCredentialPepperKey(keyring, 'missing-v1'), null);
|
||||
});
|
||||
|
||||
test('singleton compatibility is explicit and preserves its key id', () => {
|
||||
assert.deepEqual(
|
||||
createSingletonApiCredentialPepperKeyring(LEGACY, 'legacy-v1'),
|
||||
{
|
||||
schemaVersion: 1,
|
||||
activePepperKeyId: 'legacy-v1',
|
||||
keys: [{ pepperKeyId: 'legacy-v1', pepper: LEGACY }],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects widened, duplicate, missing-active, empty, and oversized keyrings', () => {
|
||||
const valid = {
|
||||
schemaVersion: 1,
|
||||
activePepperKeyId: 'legacy-v1',
|
||||
keys: [{ pepperKeyId: 'legacy-v1', pepper: LEGACY }],
|
||||
};
|
||||
for (const candidate of [
|
||||
{ ...valid, extra: true },
|
||||
{ ...valid, keys: [] },
|
||||
{
|
||||
...valid,
|
||||
keys: [valid.keys[0], valid.keys[0]],
|
||||
},
|
||||
{ ...valid, activePepperKeyId: 'missing-v1' },
|
||||
{
|
||||
...valid,
|
||||
keys: [
|
||||
valid.keys[0],
|
||||
{ pepperKeyId: 'next-v1', pepper: NEXT },
|
||||
{
|
||||
pepperKeyId: 'third-v1',
|
||||
pepper: Buffer.alloc(32, 3).toString('base64url'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...valid,
|
||||
keys: [{ ...valid.keys[0], extra: true }],
|
||||
},
|
||||
]) {
|
||||
assert.throws(
|
||||
() => normalizeApiCredentialPepperKeyring(candidate),
|
||||
/pepper keyring is invalid/,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user