Initial Commit
This commit is contained in:
55
packages/security-stack/security/vault/src/errors/codes.md
Normal file
55
packages/security-stack/security/vault/src/errors/codes.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# vault error codes
|
||||
|
||||
## Purpose
|
||||
|
||||
`codes.ts` defines the stable error-code registry for `@suaveplan/vault`. Every failure raised by the orchestration layer — locked-state rejections, bad-credential unlocks, manifest tampering, item validation, attachment / share / sync failures, and opaque-handle violations — carries one of these literal codes. The codes are frozen at module-load via `defineCodes("VAULT", [...])` so consumer code can assert on the exact strings without depending on JavaScript identifier ordering or import-time side effects.
|
||||
|
||||
The codes form the discriminated-union narrowing surface for `VaultError`. Together with the `VaultError` class they form the complete error contract: callers `import { VAULT_CODES, VaultError, type VaultErrorCode } from "@suaveplan/vault/errors"`, branch on `err.code`, and let TypeScript exhaustively check the switch.
|
||||
|
||||
## Features
|
||||
|
||||
- Two-level namespacing — `VAULT.LOCKED` for top-level lifecycle errors, `VAULT.ITEM.NOT_FOUND` for module-scoped errors. The dot-separated form is searchable and groups related failures (`ITEM.*`, `ATTACHMENT.*`, `SHARE.*`, `SYNC.*`).
|
||||
- Every code is a literal string in the `VaultErrorCode` union — no enum cost, no value coupling.
|
||||
- Hard-frozen registry — `defineCodes` returns a frozen object so consumers cannot accidentally mutate or extend the registry at runtime.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { VAULT_CODES, VaultError } from "@suaveplan/vault/errors";
|
||||
|
||||
throw new VaultError(VAULT_CODES.LOCKED, "error.vault.locked");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { VAULT_CODES, VaultError, type VaultErrorCode } from "@suaveplan/vault/errors";
|
||||
|
||||
function describe(err: VaultError): string {
|
||||
const code: VaultErrorCode = err.code;
|
||||
switch (code) {
|
||||
case VAULT_CODES.LOCKED:
|
||||
return "Vault is locked — call unlock() first.";
|
||||
case VAULT_CODES.INVALID_CREDENTIALS:
|
||||
return "Wrong password or secret key.";
|
||||
case VAULT_CODES["ITEM.NOT_FOUND"]:
|
||||
return "Item does not exist in this vault.";
|
||||
case VAULT_CODES["ITEM.VERSION_CONFLICT"]:
|
||||
return "Item was modified concurrently — refetch and retry.";
|
||||
// ... exhaustive
|
||||
default:
|
||||
return "Unknown vault error.";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `VAULT_CODES` — frozen `Record<string, "VAULT.X.Y">` literal registry.
|
||||
- `VaultErrorCode` — union of every value in `VAULT_CODES`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry is intentionally fixed in source — adding a code is a public-API change requiring an OpenSpec proposal and a coordinated release with consumers.
|
||||
- `defineCodes("VAULT", [...])` automatically prefixes each entry with `VAULT.`; the source array contains the unqualified suffix so visual scanning stays compact.
|
||||
- Sub-namespaces (`ITEM`, `ATTACHMENT`, `SHARE`, `SYNC`) match the file layout under `src/` so locating a code's emission site is mechanical.
|
||||
37
packages/security-stack/security/vault/src/errors/codes.ts
Normal file
37
packages/security-stack/security/vault/src/errors/codes.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineCodes } from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Stable error code registry for `@suaveplan/vault`.
|
||||
*
|
||||
* Every code is namespaced under `VAULT.<MODULE>.<REASON>`. The full set is
|
||||
* frozen at module-load time and may never be removed without a major version
|
||||
* bump — consumer code MAY assert on these literal strings to drive recovery
|
||||
* logic (e.g. prompting the user to re-enter their password on
|
||||
* `VAULT.INVALID_CREDENTIALS`).
|
||||
*/
|
||||
export const VAULT_CODES = defineCodes("VAULT", [
|
||||
"LOCKED",
|
||||
"INVALID_CREDENTIALS",
|
||||
"ALREADY_OPEN",
|
||||
"NOT_INITIALIZED",
|
||||
"MANIFEST_INVALID",
|
||||
"MANIFEST_VERSION_UNSUPPORTED",
|
||||
"DECRYPT_FAILED",
|
||||
"ITEM.NOT_FOUND",
|
||||
"ITEM.VALIDATION_FAILED",
|
||||
"ITEM.VERSION_CONFLICT",
|
||||
"ATTACHMENT.NOT_FOUND",
|
||||
"ATTACHMENT.TOO_LARGE",
|
||||
"SHARE.NOT_FOUND",
|
||||
"SHARE.REVOKED",
|
||||
"SYNC.FAILED",
|
||||
"OPAQUE_HANDLE_INVALID",
|
||||
"DRIVE.FAILURE",
|
||||
] as const);
|
||||
|
||||
/**
|
||||
* Union of every fully-qualified error code emitted by `vault`. Use as the
|
||||
* type parameter of {@link import("./vault-error.js").VaultError} for
|
||||
* narrowed `code` checks.
|
||||
*/
|
||||
export type VaultErrorCode = (typeof VAULT_CODES)[keyof typeof VAULT_CODES];
|
||||
@@ -0,0 +1,2 @@
|
||||
export { VAULT_CODES, type VaultErrorCode } from "./codes.js";
|
||||
export { VaultError } from "./vault-error.js";
|
||||
@@ -0,0 +1,52 @@
|
||||
# VaultError
|
||||
|
||||
## Purpose
|
||||
|
||||
`vault-error.ts` defines `VaultError`, the concrete `SuaveplanError` subclass thrown by every public path in `@suaveplan/vault`. It binds the runtime instance to a `VaultErrorCode` from the stable code registry and tags every instance with a unique brand symbol so duck-typed peers cannot impersonate a `VaultError` across module or worker boundaries.
|
||||
|
||||
This package follows the same shape as `@suaveplan/vault-sync`'s `VaultSyncError` and `@suaveplan/crypto-vault`'s `CryptoVaultError`: subclass `SuaveplanError` once, parameterise on the package's code-union, and add a brand-symbol type guard. Consumer code in the password-manager UI, browser extension, and desktop shell can pattern-match on `err instanceof VaultError` and switch over `err.code` without worrying about colliding with a foreign error class.
|
||||
|
||||
## Features
|
||||
|
||||
- Subclass of `SuaveplanError<VaultErrorCode>` — inherits structured fields (`code`, `messageKey`, `cause`, `context`).
|
||||
- Unique-symbol brand attached at construction — survives `instanceof` failures across realms.
|
||||
- `VaultError.is(value)` static type guard — safe across iframe / worker boundaries.
|
||||
- Forwards `SuaveplanErrorOptions` to the base class so `cause`, `context`, and i18n metadata flow through unchanged.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { VAULT_CODES, VaultError } from "@suaveplan/vault/errors";
|
||||
|
||||
throw new VaultError(VAULT_CODES.LOCKED, "error.vault.locked");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { VAULT_CODES, VaultError } from "@suaveplan/vault/errors";
|
||||
|
||||
try {
|
||||
await vault.items.create({ title: "" });
|
||||
} catch (err) {
|
||||
if (VaultError.is(err) && err.code === VAULT_CODES["ITEM.VALIDATION_FAILED"]) {
|
||||
// Surface the missing-field reason from `err.context` to the form.
|
||||
showValidationErrors(err.context);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class VaultError extends SuaveplanError<VaultErrorCode>` — concrete error class.
|
||||
- `constructor(code: VaultErrorCode, messageKey: string, options?: SuaveplanErrorOptions)` — instantiate with one of the codes in `VAULT_CODES` and an i18n message key.
|
||||
- `static is(value: unknown): value is VaultError` — brand-aware type guard.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The brand symbol is a `unique symbol` declared module-locally; it cannot be reproduced from outside this file even with full source access.
|
||||
- The constructor sets the brand property after calling `super()` so the inherited `code`, `messageKey`, and `context` fields are populated normally.
|
||||
- The static `is(value)` guard checks both `instanceof VaultError` and the brand property — this defends against subclasses or polyfills that satisfy `instanceof` but were not minted by this package.
|
||||
- Throwing `VaultError` is the only way the package signals a recoverable failure to callers; lifecycle, item, attachment, share, sync, and opaque-handle paths all use this class.
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { VAULT_CODES } from "./codes.js";
|
||||
import { VaultError } from "./vault-error.js";
|
||||
|
||||
describe("VaultError", () => {
|
||||
it("carries the code and resolves the message key into the error message", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = new VaultError(VAULT_CODES.LOCKED, "error.vault.locked");
|
||||
expect(err.code).toBe(VAULT_CODES.LOCKED);
|
||||
expect(err.message.length).toBeGreaterThan(0);
|
||||
expect(err.name).toBe("VaultError");
|
||||
});
|
||||
|
||||
it("forwards options to the SuaveplanError base class", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("underlying");
|
||||
const err = new VaultError(
|
||||
VAULT_CODES["ITEM.NOT_FOUND"],
|
||||
"error.vault.item.not-found",
|
||||
{
|
||||
cause,
|
||||
context: { id: "abc" },
|
||||
}
|
||||
);
|
||||
expect(err.cause).toBe(cause);
|
||||
expect(err.context).toEqual({ id: "abc" });
|
||||
});
|
||||
|
||||
it("VaultError.is identifies branded instances", { timeout: 5000 }, () => {
|
||||
const err = new VaultError(
|
||||
VAULT_CODES.MANIFEST_INVALID,
|
||||
"error.vault.manifest.invalid"
|
||||
);
|
||||
expect(VaultError.is(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("VaultError.is rejects unrelated errors and primitives", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(VaultError.is(new Error("plain"))).toBe(false);
|
||||
expect(VaultError.is("string")).toBe(false);
|
||||
expect(VaultError.is(undefined)).toBe(false);
|
||||
expect(VaultError.is(null)).toBe(false);
|
||||
expect(VaultError.is(42)).toBe(false);
|
||||
});
|
||||
|
||||
it("every code in VAULT_CODES is reachable from a constructor", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
for (const code of Object.values(VAULT_CODES)) {
|
||||
const err = new VaultError(code, "error.vault.test");
|
||||
expect(err.code).toBe(code);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { SuaveplanError, type SuaveplanErrorOptions } from "@suaveplan/error";
|
||||
import type { VaultErrorCode } from "./codes.js";
|
||||
|
||||
const VAULT_ERROR_BRAND: unique symbol = Symbol("@suaveplan/vault.error");
|
||||
|
||||
/**
|
||||
* Concrete `SuaveplanError` subclass for every failure raised by
|
||||
* `@suaveplan/vault`.
|
||||
*
|
||||
* The class is parameterised by `VaultErrorCode` so consumers can write
|
||||
* `if (err instanceof VaultError) switch (err.code) { ... }` and have the
|
||||
* compiler enforce exhaustive matches against the registry. A unique brand
|
||||
* symbol is attached to every instance so subclasses or duck-typed peers
|
||||
* cannot impersonate a `VaultError` thrown by this package.
|
||||
*/
|
||||
export class VaultError extends SuaveplanError<VaultErrorCode> {
|
||||
/** Unique brand asserting this instance was constructed by this package. */
|
||||
public readonly [VAULT_ERROR_BRAND]: true;
|
||||
|
||||
public constructor(
|
||||
code: VaultErrorCode,
|
||||
messageKey: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
) {
|
||||
super(code, messageKey, options);
|
||||
this[VAULT_ERROR_BRAND] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard verifying both `instanceof` and the brand symbol. Use for
|
||||
* defence-in-depth when handling errors that may have crossed iframe or
|
||||
* worker boundaries (which can break `instanceof`).
|
||||
*/
|
||||
public static is(value: unknown): value is VaultError {
|
||||
return (
|
||||
value instanceof VaultError &&
|
||||
(value as VaultError)[VAULT_ERROR_BRAND] === true
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user