@ultimat3/core 22.14.0 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +17 -2
- package/README.md +59 -1
- package/package.json +2 -2
- package/src/client-paths.ts +26 -6
- package/src/config-navigation.ts +122 -3
- package/src/core-error-codes.ts +5 -0
- package/src/error-render.ts +4 -2
- package/src/error-retry.ts +8 -0
- package/src/index.ts +28 -1
- package/src/logger.ts +26 -0
- package/src/page-meta.ts +7 -0
- package/src/page.ts +1 -0
- package/src/process-metrics.ts +206 -0
- package/src/seal-errors.ts +76 -0
- package/src/seal-keys.ts +121 -0
- package/src/seal.ts +259 -0
- package/src/secrets-errors.ts +21 -0
- package/src/secrets.ts +21 -11
package/src/secrets-errors.ts
CHANGED
|
@@ -84,6 +84,27 @@ export class SecretsKeyInvalidError extends UltimateError {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The same condition under the same code, for a key that is NOT the current one: a malformed entry
|
|
89
|
+
* of a ring variable (`ULTIMATE_SECRETS_RETIRED_KEYS`). Its own class because its repair is an
|
|
90
|
+
* edit of that variable — the current key is fine, and re-exporting it changes nothing — and
|
|
91
|
+
* because a class has one literal `fix:`, which is what `x errors explain` prints without an
|
|
92
|
+
* instance. A variable name that is not one never reaches the line.
|
|
93
|
+
*/
|
|
94
|
+
export class SecretsRingKeyInvalidError extends UltimateError {
|
|
95
|
+
constructor(input: { at: string; found: number; expected: number; variable: string }) {
|
|
96
|
+
const variable = ENV_VAR_NAME.test(input.variable)
|
|
97
|
+
? input.variable
|
|
98
|
+
: 'the variable the cause names';
|
|
99
|
+
super({
|
|
100
|
+
code: 'X_SECRETS_KEY_INVALID',
|
|
101
|
+
cause: `the master key in ${input.at} is ${input.found} character(s); an AES-256 key is ${input.expected} lowercase hex characters`,
|
|
102
|
+
fix: `x secrets edit # ${variable} holds ${input.expected}-character lowercase hex keys separated by commas: correct or remove the entry the cause names`,
|
|
103
|
+
meta: { at: input.at },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
/**
|
|
88
109
|
* A well-formed key that is not the one this file was sealed with. Distinguishable from tampering
|
|
89
110
|
* only because the envelope carries a key id — a domain-separated SHA-256 of the key, which is
|
package/src/secrets.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SecretsKeyInvalidError,
|
|
9
9
|
SecretsKeyMismatchError,
|
|
10
10
|
SecretsPlaintextInvalidError,
|
|
11
|
+
SecretsRingKeyInvalidError,
|
|
11
12
|
SecretsTamperedError,
|
|
12
13
|
} from './secrets-errors';
|
|
13
14
|
|
|
@@ -65,14 +66,15 @@ function decodeHex(hex: string): Uint8Array<ArrayBuffer> {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// `btoa`/`atob` rather than node:buffer — both are standard globals, and a chunk loop avoids the
|
|
68
|
-
// stack blow-up `String.fromCharCode(...bytes)` hits on a spread of any size.
|
|
69
|
-
|
|
69
|
+
// stack blow-up `String.fromCharCode(...bytes)` hits on a spread of any size. Exported for
|
|
70
|
+
// `seal.ts`, which writes the same bytes in the URL-safe alphabet — one codec, never a second.
|
|
71
|
+
export function encodeBase64(bytes: Uint8Array<ArrayBuffer>): string {
|
|
70
72
|
let binary = '';
|
|
71
73
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
72
74
|
return btoa(binary);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
function decodeBase64(text: string): Uint8Array<ArrayBuffer> {
|
|
77
|
+
export function decodeBase64(text: string): Uint8Array<ArrayBuffer> {
|
|
76
78
|
const binary = atob(text);
|
|
77
79
|
const bytes = new Uint8Array(binary.length);
|
|
78
80
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
@@ -84,15 +86,22 @@ export function generateMasterKey(): string {
|
|
|
84
86
|
return encodeHex(crypto.getRandomValues(new Uint8Array(SECRETS_KEY_BYTES)));
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
+
/**
|
|
90
|
+
* 64 lowercase hex characters, or `X_SECRETS_KEY_INVALID`. Whitespace is trimmed, never repaired.
|
|
91
|
+
* `variable` names the variable a key OTHER than the current one was read from, so the refusal's
|
|
92
|
+
* fix edits that variable instead of re-exporting a current key that is fine.
|
|
93
|
+
*/
|
|
94
|
+
export function parseMasterKey(
|
|
95
|
+
raw: string,
|
|
96
|
+
at: string,
|
|
97
|
+
variable?: string,
|
|
98
|
+
): Uint8Array<ArrayBuffer> {
|
|
89
99
|
const hex = raw.trim();
|
|
90
100
|
if (hex.length !== SECRETS_KEY_HEX_LENGTH || !HEX_KEY.test(hex)) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
101
|
+
const shape = { at, found: hex.length, expected: SECRETS_KEY_HEX_LENGTH };
|
|
102
|
+
throw variable === undefined
|
|
103
|
+
? new SecretsKeyInvalidError(shape)
|
|
104
|
+
: new SecretsRingKeyInvalidError({ ...shape, variable });
|
|
96
105
|
}
|
|
97
106
|
return decodeHex(hex);
|
|
98
107
|
}
|
|
@@ -119,7 +128,8 @@ export async function masterKeyId(key: Uint8Array<ArrayBuffer>): Promise<string>
|
|
|
119
128
|
const additionalData = (header: Omit<SecretsEnvelope, 'iv' | 'ct'>): Uint8Array<ArrayBuffer> =>
|
|
120
129
|
encoder.encode(`ultimate.secrets|v=${header.v}|alg=${header.alg}|kid=${header.kid}`);
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
/** The AES-256-GCM key object, non-extractable. Shared with `seal-keys.ts`: one import, one usage set. */
|
|
132
|
+
export const importKey = (key: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
|
|
123
133
|
crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
124
134
|
|
|
125
135
|
/**
|