hanzio 3.2.0 → 3.3.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/README.md CHANGED
@@ -9,7 +9,7 @@ logging.
9
9
 
10
10
  ```bash
11
11
  bun add hanzio
12
- # only needed for hanzio/zod, hanzio/api-wrapper, hanzio/secrets and hanzio/cli
12
+ # only needed for hanzio/zod, hanzio/api-wrapper, hanzio/secrets and hanzio/cli commands with an input
13
13
  bun add zod
14
14
  ```
15
15
 
@@ -50,6 +50,7 @@ subpath export:
50
50
  | `hanzio/format` | Cached `Intl` formatters: numbers, currency, bytes, durations, dates, relative time ([docs](src/format/README.md)) |
51
51
  | `hanzio/format/phone` | Phone numbers as people read them; needs `libphonenumber-js` ([docs](src/format/README.md#phone-numbers)) |
52
52
  | `hanzio/crypto` | Web Crypto helpers: constant-time compare, HMAC/webhook verification, sealing, tokens ([docs](src/crypto/README.md)) |
53
+ | `hanzio/crypto/node` | Synchronous sealing on `node:crypto`, same envelope as `hanzio/crypto` ([docs](src/crypto/README.md#synchronous-sealing-hanziocryptonode)) |
53
54
  | `hanzio/storage` | Typed localStorage/AsyncStorage items that never throw ([docs](src/storage/README.md)) |
54
55
  | `hanzio/i18n` | Typed `{placeholder}` interpolation, translators, `TypedTranslate` and locale registries ([docs](src/i18n/README.md)) |
55
56
  | `hanzio/i18n/vue` | vue-i18n setup with locale persistence and a typed composer ([docs](src/i18n/README.md#vue-i18n)) |
@@ -1,6 +1,8 @@
1
- import { z } from 'zod';
2
- /** Thrown for bad argv. The CLI prints it with a `--help` hint and exits 2. */
1
+ import type { z } from 'zod';
2
+ /** Thrown for bad argv; `cli.run()` turns it into a `CliUsageError`. */
3
3
  export declare class UsageError extends Error {
4
+ readonly kind: 'missing-command' | 'unknown-command' | 'invalid-arguments';
5
+ constructor(message: string, kind?: 'missing-command' | 'unknown-command' | 'invalid-arguments');
4
6
  }
5
7
  /** One key of an input or globals object, read as a flag or a positional. */
6
8
  export type Field = {
@@ -13,17 +15,25 @@ export type Field = {
13
15
  hasDefault: boolean;
14
16
  defaultValue: unknown;
15
17
  description?: string;
18
+ /** One-letter short flag. */
16
19
  alias?: string;
17
- env?: string;
20
+ /** Extra long flag names, from multi-character aliases. */
21
+ aliases: string[];
22
+ env: string[];
23
+ /** `--name` alone is `true`, `--name=value` is the value. */
24
+ optionalValue: boolean;
18
25
  choices?: string[];
19
26
  };
27
+ /** The `parseArgs` option name. A `noX` key is its own flag, never `--x` negated. */
28
+ export declare const optionName: (field: Field) => string;
20
29
  export declare function fieldsOf(schema: z.ZodObject | undefined): Field[];
21
30
  /**
22
31
  * Builds the raw object Zod validates: flag values and positionals by key,
23
- * numbers coerced, `.meta({ env })` as fallback, and a missing required
24
- * boolean or array read as `false` or `[]`.
32
+ * numbers coerced, `.meta({ env })` as fallback (blank variables count as
33
+ * unset), and a missing required boolean or array read as `false` or `[]`.
34
+ * Extra positionals are a usage error unless `strict` is false.
25
35
  */
26
- export declare function readFields(fields: Field[], values: Record<string, string | boolean | (string | boolean)[] | undefined>, positionals: string[], env: Record<string, string | undefined>): Record<string, unknown>;
36
+ export declare function readFields(fields: Field[], values: Record<string, string | boolean | (string | boolean)[] | undefined>, positionals: string[], env: Record<string, string | undefined>, strict?: boolean): Record<string, unknown>;
27
37
  export declare function fieldLabel(field: Field): string;
28
38
  export declare function fieldHelp(field: Field): string;
29
39
  /** Closest candidate within a few edits, for "Did you mean ...?" hints. */
@@ -3,10 +3,16 @@ declare module 'zod' {
3
3
  interface GlobalMeta {
4
4
  /** hanzio/cli: read this key from positional arguments, in key order. */
5
5
  positional?: boolean;
6
- /** hanzio/cli: one-letter short flag, e.g. `'r'` for `-r`. */
7
- alias?: string;
8
- /** hanzio/cli: environment variable read when the flag is missing. */
9
- env?: string;
6
+ /**
7
+ * hanzio/cli: other names for the flag: one letter for `-r`, longer for
8
+ * `--region-name`.
9
+ */
10
+ alias?: string | readonly string[];
11
+ /**
12
+ * hanzio/cli: environment variables read in order when the flag is
13
+ * missing. Blank values count as unset.
14
+ */
15
+ env?: string | readonly string[];
10
16
  }
11
17
  }
12
18
  declare const commandKey: unique symbol;
@@ -30,6 +36,25 @@ export type CliIO = {
30
36
  stderr?: CliWriter;
31
37
  env?: Record<string, string | undefined>;
32
38
  };
39
+ /**
40
+ * Bad arguments, passed to `defineCli({ onError })`. `help` is the help of
41
+ * the group or command the arguments were for, ending in a newline.
42
+ */
43
+ export declare class CliUsageError extends CliError {
44
+ readonly kind: 'missing-command' | 'unknown-command' | 'invalid-arguments';
45
+ readonly path: readonly string[];
46
+ readonly help: string;
47
+ constructor(message: string, details: Pick<CliUsageError, 'kind' | 'path' | 'help'>);
48
+ }
49
+ /** What `defineCli({ onError })` gets besides the error. */
50
+ export type CliErrorIO = {
51
+ readonly stdout: CliWriter;
52
+ readonly stderr: CliWriter;
53
+ readonly env: Record<string, string | undefined>;
54
+ readonly json: boolean;
55
+ /** Prints the error the default way and returns its exit code. */
56
+ print(): number;
57
+ };
33
58
  export type CommandContext<C = unknown> = {
34
59
  /** What `defineCli({ context })` returned. */
35
60
  readonly context: C;
@@ -52,8 +77,11 @@ export type CommandContext<C = unknown> = {
52
77
  export type Command<C = unknown> = {
53
78
  readonly [commandKey]: true;
54
79
  readonly description?: string;
80
+ readonly usage?: string;
81
+ readonly examples?: readonly string[];
55
82
  readonly input?: AnyObject;
56
83
  readonly raw?: boolean;
84
+ readonly strict?: boolean;
57
85
  readonly run?: (input: never, ctx: CommandContext<C>) => unknown;
58
86
  readonly output?: (result: never, ctx: CommandContext<C>) => unknown;
59
87
  readonly load?: LazyCommand<C>;
@@ -88,6 +116,22 @@ export type CliConfig<G extends AnyObject, C> = {
88
116
  }) => unknown;
89
117
  /** A command (single-command CLI) or nested groups of commands. */
90
118
  commands: CommandNode<C>;
119
+ /**
120
+ * The root command to run when no command name is given, or when the first
121
+ * argument is a flag that isn't a global. A function reads it from `env`.
122
+ */
123
+ defaultCommand?: string | ((env: Record<string, string | undefined>) => string | undefined);
124
+ /** Resolves a command name the group doesn't have, e.g. from config. */
125
+ unknownCommand?: (name: string, path: readonly string[]) => CommandNode<C> | undefined | Promise<CommandNode<C> | undefined>;
126
+ /** Rewrites a help screen; `path` is `[]` for the root. */
127
+ help?: (text: string, info: {
128
+ path: readonly string[];
129
+ }) => string | Promise<string>;
130
+ /**
131
+ * Handles every error, usage errors included (`CliUsageError`). Return the
132
+ * exit code, or `undefined` for `io.print()`.
133
+ */
134
+ onError?: (error: unknown, io: CliErrorIO) => number | undefined | Promise<number | undefined>;
91
135
  } & (undefined extends C ? {
92
136
  context?: ContextFunction<G, C>;
93
137
  } : {
@@ -101,14 +145,22 @@ type ContextFunction<G extends AnyObject, C> = (globals: z.output<G>) => C | Pro
101
145
  * printed by `output` (default: strings as-is, anything else as JSON).
102
146
  */
103
147
  export declare function defineCommand<S extends AnyObject = EmptyObject, R = unknown, C = unknown>(definition: {
148
+ /** The first line is the summary in command lists; help shows it all. */
104
149
  description?: string;
150
+ /** Replaces the generated arguments in usage lines, e.g. `'[name...] [--all]'`. */
151
+ usage?: string;
152
+ /** Command lines shown under "Examples:" in the command's help. */
153
+ examples?: readonly string[];
105
154
  input?: S;
155
+ /** `false` ignores unknown flags and extra positionals. */
156
+ strict?: boolean;
106
157
  run: (input: z.output<S>, ctx: CommandContext<C>) => R;
107
158
  output?: (result: Awaited<R>, ctx: CommandContext<C>) => unknown;
108
159
  }): Command<C>;
109
160
  /** A command that skips parsing: everything after its name is in `ctx.argv`. */
110
161
  export declare function defineCommand<R = unknown, C = unknown>(definition: {
111
162
  description?: string;
163
+ usage?: string;
112
164
  raw: true;
113
165
  run: (input: Record<string, never>, ctx: CommandContext<C>) => R;
114
166
  output?: (result: Awaited<R>, ctx: CommandContext<C>) => unknown;
@@ -116,6 +168,7 @@ export declare function defineCommand<R = unknown, C = unknown>(definition: {
116
168
  /** A lazy command whose description shows in help before it loads. */
117
169
  export declare function defineCommand<C = unknown>(definition: {
118
170
  description?: string;
171
+ usage?: string;
119
172
  load: LazyCommand<C>;
120
173
  }): Command<C>;
121
174
  export declare function defineCli<G extends AnyObject = EmptyObject, C = undefined>(config: CliConfig<G, C>): Cli;
@@ -0,0 +1,17 @@
1
+ import { type SealerOptions } from './sealer';
2
+ export { SealerError, type SealerErrorCode, type SealerOptions } from './sealer';
3
+ export type SealerSync = {
4
+ seal(plaintext: string): string;
5
+ open(sealed: string): string;
6
+ sealJson(value: unknown): string;
7
+ openJson<T = unknown>(sealed: unknown): T;
8
+ };
9
+ /**
10
+ * AES-256-GCM sealing with a random 96-bit IV per value, synchronously.
11
+ *
12
+ * @example
13
+ * const secrets = createSealerSync({ secret: process.env.APP_SECRET!, info: 'app.credentials.v1' })
14
+ * const stored = secrets.sealJson({ accessToken })
15
+ * const { accessToken } = secrets.openJson<{ accessToken: string }>(stored)
16
+ */
17
+ export declare function createSealerSync(options: SealerOptions): SealerSync;
@@ -6,20 +6,52 @@ export type SealerOptions = {
6
6
  salt?: string;
7
7
  /** HKDF info; use a distinct value per purpose. Default: `'hanzio.sealer.aes-256-gcm.v1'` */
8
8
  info?: string;
9
+ /**
10
+ * Older `{ salt, info }` pairs that `open` also tries, so values sealed before
11
+ * a rename stay readable. `seal` always uses `salt` and `info`.
12
+ */
13
+ fallbacks?: readonly {
14
+ salt?: string;
15
+ info?: string;
16
+ }[];
17
+ /**
18
+ * `open` and `openJson` return a value that was never sealed unchanged
19
+ * (`{}`, a legacy plaintext) instead of throwing. Malformed envelopes and
20
+ * other envelope versions still throw.
21
+ */
22
+ passthroughUnsealed?: boolean;
9
23
  };
10
24
  export type Sealer = {
11
25
  seal(plaintext: string): Promise<string>;
12
26
  open(sealed: string): Promise<string>;
13
27
  sealJson(value: unknown): Promise<string>;
14
- openJson<T = unknown>(sealed: string): Promise<T>;
28
+ openJson<T = unknown>(sealed: unknown): Promise<T>;
15
29
  };
16
30
  export type SealerErrorCode = 'malformed' | 'unsupported_version' | 'decrypt_failed';
17
31
  export declare class SealerError extends Error {
18
32
  readonly code: SealerErrorCode;
19
33
  constructor(code: SealerErrorCode);
20
34
  }
35
+ /** The secret's bytes and every HKDF context `open` tries, primary first. */
36
+ export declare function sealerKeyInputs(options: SealerOptions): {
37
+ secret: Uint8Array<ArrayBuffer>;
38
+ contexts: {
39
+ salt: string;
40
+ info: string;
41
+ }[];
42
+ };
43
+ export declare function formatEnvelope(iv: Uint8Array, tag: Uint8Array, ciphertext: Uint8Array): string;
44
+ export type Envelope = {
45
+ iv: Uint8Array<ArrayBuffer>;
46
+ tag: Uint8Array<ArrayBuffer>;
47
+ ciphertext: Uint8Array<ArrayBuffer>;
48
+ };
49
+ /** `null` means `sealed` was never sealed and `passthroughUnsealed` allows it. */
50
+ export declare function parseEnvelope(sealed: unknown, passthroughUnsealed?: boolean): Envelope | null;
51
+ export declare function jsonToSeal(value: unknown): string;
21
52
  /**
22
53
  * AES-256-GCM sealing with a random 96-bit IV per value. Envelope:
23
- * `enc:v1:<b64url(iv)>.<b64url(tag)>.<b64url(ciphertext)>`.
54
+ * `enc:v1:<b64url(iv)>.<b64url(tag)>.<b64url(ciphertext)>`. Async, on Web
55
+ * Crypto; `hanzio/crypto/node` has a synchronous twin with the same envelope.
24
56
  */
25
57
  export declare function createSealer(options: SealerOptions): Sealer;