Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View 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.

View 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];

View File

@@ -0,0 +1,2 @@
export { VAULT_CODES, type VaultErrorCode } from "./codes.js";
export { VaultError } from "./vault-error.js";

View File

@@ -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.

View File

@@ -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);
}
});
});

View File

@@ -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
);
}
}