@k9o/kibo 1.2.0 → 1.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/src/crypto.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ZK encrypt/decrypt — the thin TS wrapper over the bound crypto core
3
- * (`kibo-kms-rest-ffi`, piece C) compiled to wasm. ZERO crypto here: every
3
+ * (`kibo-kms-rest-ffi`) compiled to wasm. ZERO crypto here: every
4
4
  * wrap/unwrap, envelope framing, posture-floor enforcement, and KMS round-trip
5
5
  * lives in the audited Rust core. This layer only marshals the typed input
6
6
  * across the wasm boundary and maps the core's error string → `KError`.
@@ -9,12 +9,13 @@
9
9
  * objects via serde-wasm-bindgen, and rejecting with the `ZkFfiError` display
10
10
  * string (`"<K9O_CODE>: <message>"`, optionally with a posture-violation tail).
11
11
  *
12
- * Composes into `@k9o/core` as `client.kibo.encrypt`/`decrypt`; the F.5
13
- * `vault.zkDeks.{store,fetch}` surface (envelope persistence) is separate +
14
- * decoupled (the core never sees the store).
12
+ * These are standalone exports (`import { encrypt, decrypt } from "@k9o/kibo"`), not methods of the client: they run
13
+ * locally against the customer's own KMS and make no platform call. The `vault.zkDeks.{store,fetch}` surface
14
+ * (envelope persistence) is separate and decoupled (the core never sees the store).
15
15
  */
16
16
 
17
17
  import { KError } from "@k9o/core/errors";
18
+ import { loadZkGlue, nodeDeps } from "./wasm-loader.ts";
18
19
  import type {
19
20
  KiboEncryptInput,
20
21
  KiboEncryptOutput,
@@ -40,34 +41,20 @@ export function setZkWasm(mod: ZkWasm): void {
40
41
  }
41
42
 
42
43
  /**
43
- * Lazily load the bound core wasm. ONE all-provider artifact is generated from
44
- * `kibo-kms-rest-ffi` (the core compiles every v2 KMS provider in; the
45
- * `provider` input field selects at call time), vendored under `wasm/` by the
46
- * publish pipeline and gitignored in-repo. The specifier is assembled at
47
- * runtime so the bundler/typechecker must not hard-resolve it — the contract
48
- * is the `ZkWasm` interface above.
44
+ * Lazily load the bound core wasm. ONE all-provider artifact is generated from `kibo-kms-rest-ffi` (the core compiles
45
+ * every KMS provider in; the `provider` input field selects at call time) and vendored under `wasm/` by the publish
46
+ * pipeline. See `wasm-loader.ts` for how the CommonJS glue is loaded from this ESM package and for the failure modes.
49
47
  */
50
48
  async function loadWasm(): Promise<ZkWasm> {
51
49
  if (cachedWasm) return cachedWasm;
52
- const pkg = ["..", "wasm", "kibo_kms_rest_ffi.js"].join("/");
53
- try {
54
- cachedWasm = (await import(pkg)) as unknown as ZkWasm;
55
- } catch {
56
- // A package published without its native core (no bundle at publish time). Say so; never surface a bare
57
- // module-resolution error, and do not cache the failure.
58
- throw new KError({
59
- code: "KIBO_WASM_ERROR",
60
- message: "the Kibo zero-knowledge core (wasm) is not bundled in this build of @k9o/kibo; encrypt and decrypt are unavailable",
61
- context: {},
62
- recommendation: "Install a release of @k9o/kibo that ships the core, or load it yourself.",
63
- });
64
- }
50
+ // A failure is not cached: the next call tries again.
51
+ cachedWasm = await loadZkGlue(nodeDeps(import.meta.url));
65
52
  return cachedWasm;
66
53
  }
67
54
 
68
55
  /**
69
56
  * Encrypt `input.plaintext` via the customer KMS, client-side. Returns the
70
- * opaque ZKFE envelope — store it via `vault.zkDeks.store` (F.5) or keep it
57
+ * opaque ZKFE envelope — store it via `vault.zkDeks.store` or keep it
71
58
  * yourself; it is meaningless to the platform.
72
59
  */
73
60
  export async function encrypt(input: KiboEncryptInput): Promise<KiboEncryptOutput> {
package/src/index.ts CHANGED
@@ -1,19 +1,16 @@
1
1
  /**
2
- * `@k9o/kibo` — Zero-Knowledge BYOK/HYOK vault SDK (client-side).
2
+ * `@k9o/kibo` - Zero-Knowledge BYOK/HYOK vault SDK.
3
3
  *
4
- * Composes into `@k9o/core` as `client.kibo`. Two surfaces:
5
- * - F.5 ZK-DEK store/fetch — `vault.zkDeks.{store,fetch}`;
6
- * decoupled, gateway-only opaque-blob persistence.
7
- * - ZK `encrypt`/`decrypt` (/) — thin wrappers over the
8
- * audited core (piece C, `kibo-kms-rest-ffi` compiled to wasm).
4
+ * Two surfaces:
5
+ * - the vault client (`createKiboClient`; composed into `@k9o/core` as `client.kibo`): `vault.secrets.*` and
6
+ * `vault.zkDeks.{store,fetch}`, plain REST calls to the platform.
7
+ * - standalone `encrypt` / `decrypt`: local envelope encryption against the customer's own KMS, in WASM, with no
8
+ * platform call. They are not methods of the client. `setZkWasm` supplies the WASM core yourself.
9
9
  *
10
- * Zero crypto in this package: every wrap/unwrap, envelope framing, and
11
- * posture-floor enforcement lives in the bound core; the SDK only marshals
12
- * typed input across the wasm boundary + ships/retrieves opaque blobs.
10
+ * No cryptography lives in this package: wrapping, envelope framing and posture-floor enforcement are in the compiled core.
13
11
  */
14
12
 
15
13
  export { createKiboClient, type KiboClient } from "./client-factory.ts";
16
- export { makeVaultNamespace } from "./vault.ts";
17
14
  export type {
18
15
  KmsProvider, StoreZkDekInput, ZkDekHandle, ZkDekRecord,
19
16
  SecretVisibility, CreateSecretInput, SecretSummary,
@@ -21,9 +18,7 @@ export type {
21
18
  UpdateSecretInput, UpdateSecretResult, RotateSecretResult, MoveSecretResult,
22
19
  } from "./types.ts";
23
20
 
24
- // ZK encrypt/decrypt (bound-core wasm). The ergonomic client-bound surface
25
- // (gathering provider/posture/key-ref from config) composes per the
26
- // method-shape contract; these are the FFI-mapped primitives it builds on.
21
+ // Standalone zero-knowledge encrypt/decrypt (bound-core wasm).
27
22
  export { encrypt, decrypt, setZkWasm, type ZkWasm } from "./crypto.ts";
28
23
  export type {
29
24
  KiboPosture, KiboStrategy, KiboOracleMode, KiboAttestationTier, KiboMinPosture,
package/src/types.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * `@k9o/kibo` — Zero-Knowledge BYOK/HYOK vault SDK types.
3
3
  *
4
- * Surface: the ZK-DEK store/fetch. The wrapped
5
- * envelope is produced client-side by the audited ZK core (the
6
- * `kibo-kms-rest` binding); this SDK ships it **verbatim and opaque** and never
7
- * reads its interior — the strict-ZK invariant + envelope framing live in the
8
- * core, never here.
4
+ * Surface: the ZK-DEK store/fetch and the secrets family. The wrapped
5
+ * envelope is produced client-side by the ZK core (compiled to wasm); this SDK ships it
6
+ * **verbatim and opaque** and never reads its interior.
9
7
  */
10
8
 
11
9
  /** Customer KMS/HSM provider — non-secret routing metadata (mirrors the ZK FFI provider selector). */
@@ -42,24 +40,19 @@ export type ZkDekRecord = {
42
40
  createdAt: string;
43
41
  };
44
42
 
45
- /** secret visibility — write ops (create/update) stay owner-only regardless of this tag. */
43
+ /** Secret visibility. Write operations (create/update) stay owner-only regardless of this tag. */
46
44
  export type SecretVisibility =
47
45
  | { kind: "private" }
48
46
  | { kind: "shared"; principals?: string[]; roles?: string[] };
49
47
 
50
- /** Input to create a vault secret — `POST /@<projectSlug>/vault/secrets/<name>?region=<region>`. */
48
+ /** Input to create a vault secret: `POST /@<projectSlug>/vault/secrets/<name>`. */
51
49
  export type CreateSecretInput = {
52
50
  /** Hierarchical path the secret is addressed under, e.g. `byos/s3-prod` (also the `:name` URL segment). */
53
51
  name: string;
54
- /** The plaintext to seal, as a regular (non-base64) string — `vault.secrets.create` base64-encodes
55
- * it for the wire itself (live-verified against staging: the route's OpenAPI description says
56
- * "the plaintext to seal" but a raw non-base64 `value` is rejected as unparseable). */
52
+ /** The plaintext to seal, as a regular (non-base64) string; `vault.secrets.create` base64-encodes it for the wire. */
57
53
  value: string;
58
- /** The authoritative region for this vault write (sent as `?region=`), REQUIRED — the vault-forward
59
- * proxy fail-closes without it (live-verified against staging, not documented in the route's
60
- * OpenAPI schema as of this writing). Distinct from any region carried inside `value`'s own
61
- * envelope. */
62
- region: string;
54
+ /** Optional. The gateway sets the residency region of the write itself and discards this; it is sent only when given. */
55
+ region?: string;
63
56
  visibility?: SecretVisibility;
64
57
  metadata?: Record<string, string>;
65
58
  };
@@ -83,8 +76,7 @@ export type SecretSummary = {
83
76
  metadata: Record<string, string>;
84
77
  };
85
78
 
86
- /** `client.kibo.vault.secrets.list()` input — none of these need `region` (live-verified: unlike every
87
- * other secrets operation, list is NOT region-gated). */
79
+ /** `client.kibo.vault.secrets.list()` input. */
88
80
  export type ListSecretsInput = {
89
81
  /** Filter to a path prefix. */
90
82
  prefix?: string;
@@ -97,7 +89,7 @@ export type ListSecretsResult = {
97
89
  nextCursor: string | null;
98
90
  };
99
91
 
100
- /** releasing-node observed attestation tier (K4) — present on every reveal. */
92
+ /** The releasing node's observed attestation tier; present on every reveal. */
101
93
  export type AttestationTier = {
102
94
  /** `attestation:destination:region` trust-tier URN. */
103
95
  urn: string;
@@ -126,19 +118,16 @@ export type RevealSecretResult = {
126
118
 
127
119
  /** `update()` input. */
128
120
  export type UpdateSecretInput = {
129
- /** The authoritative region for this write (sent as `?region=`), REQUIRED — same live-verified gate
130
- * as `create`. */
131
- region: string;
121
+ /** Optional. The gateway sets the residency region of the write itself and discards this; it is sent only when given. */
122
+ region?: string;
132
123
  /** New plaintext — bumps `version`. The SDK base64-encodes it for the wire. */
133
124
  value: string;
134
125
  metadata?: Record<string, string>;
135
126
  };
136
127
 
137
128
  /**
138
- * `update()` result — 🔴 live-verified NARROWER than the documented `SecretSummary`: the gateway
139
- * returns only `{id, name, version}` on a real PUT, not the full summary the OpenAPI schema claims.
140
- * Typed to what's actually observed rather than the aspirational doc shape; call `get()` afterward if
141
- * the full summary is needed.
129
+ * `update()` result: the gateway returns only `{id, name, version}`, narrower than the OpenAPI `SecretSummary`.
130
+ * Call `get()` afterward if the full summary is needed.
142
131
  */
143
132
  export type UpdateSecretResult = {
144
133
  id: string;
@@ -147,9 +136,8 @@ export type UpdateSecretResult = {
147
136
  };
148
137
 
149
138
  /**
150
- * `rotate()` result — 🔴 live-verified shape, not documented anywhere in the OpenAPI schema (the route
151
- * only declares `t.Union([SecretSummary, RevealChallengeResponse])`, which this matches neither).
152
- * `ledgerSequenceNumber` is undocumented; kept as-observed.
139
+ * `rotate()` result: not described by the OpenAPI schema (which declares a summary or a reveal challenge for the
140
+ * route); typed to what the gateway returns. `ledgerSequenceNumber` is undocumented.
153
141
  */
154
142
  export type RotateSecretResult = {
155
143
  id: string;
@@ -160,9 +148,8 @@ export type RotateSecretResult = {
160
148
  };
161
149
 
162
150
  /**
163
- * `move()` result — 🔴 live-verified shape, also not the documented `SecretSummary`: no `version`, and
164
- * an undocumented `event` field (`"SECRET_PATH_CHANGED"` observed; typed as `string` since only one
165
- * sample was seen and a closed enum can't be asserted from one observation).
151
+ * `move()` result: not the OpenAPI `SecretSummary`; it has no `version` and carries an undocumented `event`
152
+ * field (`"SECRET_PATH_CHANGED"` observed; typed as `string`).
166
153
  */
167
154
  export type MoveSecretResult = {
168
155
  id: string;
package/src/vault.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  /**
2
- * `client.kibo.vault.*` — the Kibo vault surface.
2
+ * `client.kibo.vault.*` - the Kibo vault surface.
3
3
  *
4
- * `vault.zkDeks.{store,fetch}` — member-plane,
5
- * project-scoped storage of the opaque customer-wrapped DEK envelope. Zero
6
- * crypto SDK-side: this is a plain authenticated REST round-trip carrying an
7
- * opaque blob (the wrapping happens in the ZK core, piece C). The encryption
8
- * surface (`kibo.encrypt`/`decrypt`) is provided by the ZK core binding when it is
9
- * bundled.
4
+ * `vault.zkDeks.{store,fetch}` store and fetch the opaque customer-wrapped DEK envelope, project-scoped. There is
5
+ * no cryptography in this client: it is a plain authenticated REST round-trip carrying an opaque blob (the
6
+ * wrapping happens in the standalone `encrypt`/`decrypt`, which never call the platform).
7
+ * `vault.secrets.*` is the typed secrets family.
10
8
  */
11
9
 
12
10
  import { request, type HttpClientConfig } from "@k9o/core/http";
@@ -20,9 +18,7 @@ import type {
20
18
  RotateSecretResult, MoveSecretResult,
21
19
  } from "./types.ts";
22
20
 
23
- /** UTF-8-safe base64 encode, usable across browser/Node/Bun (unlike `Buffer`) — the vault-secrets wire
24
- * `value` field is base64 (live-verified; the OpenAPI "plaintext to seal" description does not mean
25
- * raw-on-the-wire). */
21
+ /** UTF-8-safe base64 encode, usable across browser/Node/Bun (unlike `Buffer`). The wire `value` field is base64. */
26
22
  function encodeBase64Utf8(plaintext: string): string {
27
23
  return btoa(unescape(encodeURIComponent(plaintext)));
28
24
  }
@@ -32,7 +28,7 @@ function decodeBase64Utf8(b64: string): string {
32
28
  return decodeURIComponent(escape(atob(b64)));
33
29
  }
34
30
 
35
- /** Both `vault.zkDeks` and `vault.secrets` require a project context (member-plane, /). */
31
+ /** Both `vault.zkDeks` and `vault.secrets` require a project context (member-plane). */
36
32
  function requireProjectSlug(config: HttpClientConfig): string {
37
33
  if (config.projectSlug === undefined || config.projectSlug.length === 0) {
38
34
  throw new KError({
@@ -45,9 +41,20 @@ function requireProjectSlug(config: HttpClientConfig): string {
45
41
  return config.projectSlug;
46
42
  }
47
43
 
44
+ /** Bound on the pages `secrets.get` scans for an exact name. */
45
+ const MAX_GET_PAGES = 20;
46
+
47
+ /** @internal */
48
48
  export function makeVaultNamespace(config: HttpClientConfig) {
49
+ const listSecrets = async (input?: ListSecretsInput): Promise<Envelope<ListSecretsResult>> =>
50
+ request<ListSecretsResult>(config, {
51
+ method: "GET",
52
+ path: projectPath(requireProjectSlug(config), "vault/secrets"),
53
+ query: { prefix: input?.prefix, cursor: input?.cursor, limit: input?.limit },
54
+ });
55
+
49
56
  return {
50
- /** ZK-DEK opaque store/fetch (F.5). */
57
+ /** Opaque store/fetch of a customer-wrapped DEK envelope. */
51
58
  zkDeks: {
52
59
  /**
53
60
  * Store an opaque customer-wrapped DEK envelope.
@@ -76,57 +83,49 @@ export function makeVaultNamespace(config: HttpClientConfig) {
76
83
  },
77
84
 
78
85
  /**
79
- * typed secrets surface (`services/gateway/src/routes/vault-secrets.ts`) — the full family:
80
- * list/get/create/revealChallenge/reveal/update/rotate/move/delete. Every operation on a NAMED
81
- * secret (everything except `list`) requires `region` — live-verified against staging, not
82
- * documented in the route's OpenAPI schema. `list` is the one exception (verified it does NOT
83
- * need `region`).
86
+ * Typed secrets surface (`/@<projectSlug>/vault/secrets`): list / get / create / revealChallenge / reveal /
87
+ * update / rotate / move / delete.
88
+ *
89
+ * `region`: the gateway sets the residency region of every vault call itself and discards a caller-supplied
90
+ * `region` query parameter, so it is optional on every method. When given, it is still sent (older gateways
91
+ * required it).
84
92
  *
85
- * Response shapes for `update`/`rotate`/`move` are typed to what the gateway ACTUALLY returns
86
- * (live-verified), not the OpenAPI's documented (and inaccurate for these three) `SecretSummary` —
87
- * see the 🔴 notes on `UpdateSecretResult`/`RotateSecretResult`/`MoveSecretResult` in `types.ts`.
93
+ * Response shapes for `update`/`rotate`/`move` are typed to what the gateway returns, not the OpenAPI's
94
+ * `SecretSummary` (see `UpdateSecretResult`/`RotateSecretResult`/`MoveSecretResult` in `types.ts`).
88
95
  */
89
96
  secrets: {
90
- /** List secrets (metadata only — never lists ciphertext). `GET /@<projectSlug>/vault/secrets`.
91
- * The one secrets operation that does NOT need `region` (live-verified). */
92
- async list(input?: ListSecretsInput): Promise<Envelope<ListSecretsResult>> {
93
- return request<ListSecretsResult>(config, {
94
- method: "GET",
95
- path: projectPath(requireProjectSlug(config), "vault/secrets"),
96
- query: { prefix: input?.prefix, cursor: input?.cursor, limit: input?.limit },
97
- });
98
- },
97
+ /** List secrets (metadata only, never ciphertext). `GET /@<projectSlug>/vault/secrets`. */
98
+ list: listSecrets,
99
99
 
100
100
  /**
101
- * Read a secret's metadata (never its value — use `revealChallenge`+`reveal` for that).
102
- * `GET /@<projectSlug>/vault/secrets/<name>?region=<region>`.
101
+ * Read one secret's metadata (never its value; use `revealChallenge` + `reveal` for that).
103
102
  *
104
- * 🔴 KNOWN LIVE BUG (staging, 2026-09-05): this call currently fails with
105
- * `K9O_REVEAL_CHALLENGE_REQUIRED` on every attempt — the gateway/kibo does not honor the
106
- * documented "no `X-K9O-Reveal-Challenge` header ⇒ return metadata" behavior; it demands a
107
- * challenge unconditionally. Implemented per the documented contract (this is a backend defect,
108
- * not a client workaround); `list({prefix: exactName})` is the current working alternative for a
109
- * single secret's metadata until the gateway/kibo side is fixed.
103
+ * Vault answers a `GET /@<projectSlug>/vault/secrets/<name>` only as a reveal: with no
104
+ * `X-K9O-Reveal-Challenge` header it refuses with 401 `K9O_REVEAL_CHALLENGE_REQUIRED`, whatever the caller's
105
+ * rights. There is no plain metadata read on that route, so `get` reads the metadata the route that exists
106
+ * for it returns: it lists by the exact name as prefix and picks the exact match. It throws `K9O_NOT_FOUND`
107
+ * (status 404) when no secret has that name.
110
108
  */
111
- async get(name: string, opts: { region: string }): Promise<Envelope<SecretSummary>> {
112
- return request<SecretSummary>(config, {
113
- method: "GET",
114
- path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
115
- query: { region: opts.region },
109
+ async get(name: string, _opts?: { region?: string }): Promise<Envelope<SecretSummary>> {
110
+ let cursor: string | undefined;
111
+ for (let page = 0; page < MAX_GET_PAGES; page++) {
112
+ const env = await listSecrets({ prefix: name, ...(cursor !== undefined ? { cursor } : {}), limit: 100 });
113
+ const hit = env.data.secrets.find((s) => s.name === name);
114
+ if (hit !== undefined) return { ...env, data: hit };
115
+ if (env.data.nextCursor === null || env.data.nextCursor === undefined) break;
116
+ cursor = env.data.nextCursor;
117
+ }
118
+ throw new KError({
119
+ code: "K9O_NOT_FOUND",
120
+ message: `No secret named '${name}' in this project.`,
121
+ context: { name },
122
+ status: 404,
116
123
  });
117
124
  },
118
125
 
119
126
  /**
120
127
  * Create a secret under the caller's own auth.
121
- * `POST /@<projectSlug>/vault/secrets/<name>?region=<region>` → the `sec_<base62>` summary.
122
- *
123
- * 🔴 `region` is REQUIRED on the query string, live-verified against staging — the vault-forward
124
- * proxy fail-closes with `K9O_INVALID_FIELD_VALUE` ("this request requires an authoritative
125
- * region") without it. Distinct from any `region` carried inside `input.value`'s envelope (e.g.
126
- * the BYOS aws_sigv4 STS-signing region): this one scopes the vault WRITE itself, mirroring the
127
- * gateway's `gatewayRegion()` residency guard (region-guard.ts) that closed a 2026-08 wrong-region
128
- * incident for the audit path — the request-shape unit tests never caught this because they don't
129
- * exercise a real gateway.
128
+ * `POST /@<projectSlug>/vault/secrets/<name>` -> the `sec_<base62>` summary.
130
129
  */
131
130
  async create(input: CreateSecretInput): Promise<Envelope<SecretSummary>> {
132
131
  const { name, region, value, ...rest } = input;
@@ -134,29 +133,27 @@ export function makeVaultNamespace(config: HttpClientConfig) {
134
133
  method: "POST",
135
134
  path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
136
135
  query: { region },
137
- // Live-verified against staging: the wire `value` MUST be base64 — a raw plaintext string
138
- // (matching the route's own "the plaintext to seal" OpenAPI description literally) is
139
- // rejected with K9O_INVALID_FIELD_VALUE "Request body could not be parsed." `input.value`
140
- // stays the caller-facing plaintext; base64 is purely a wire-transport detail handled here.
136
+ // The wire `value` is base64 (a raw string is rejected as unparseable). `input.value` stays the
137
+ // caller-facing plaintext; base64 is purely a wire detail handled here.
141
138
  body: { ...rest, value: encodeBase64Utf8(value) },
142
139
  });
143
140
  },
144
141
 
145
- /** Mint a single-use reveal challenge — present it as `X-K9O-Reveal-Challenge` on the
146
- * immediately-following `reveal()` call. `POST .../<name>?op=reveal-challenge&region=<region>`. */
147
- async revealChallenge(name: string, opts: { region: string }): Promise<Envelope<RevealChallenge>> {
142
+ /** Mint a single-use reveal challenge; pass it to the immediately-following `reveal()`.
143
+ * `POST .../<name>?op=reveal-challenge`. The optional `reason` is accepted by the gateway and not forwarded. */
144
+ async revealChallenge(name: string, opts?: { region?: string; reason?: string }): Promise<Envelope<RevealChallenge>> {
148
145
  return request<RevealChallenge>(config, {
149
146
  method: "POST",
150
147
  path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
151
- query: { op: "reveal-challenge", region: opts.region },
148
+ query: { op: "reveal-challenge", region: opts?.region },
149
+ ...(opts?.reason !== undefined ? { body: { reason: opts.reason } } : {}),
152
150
  });
153
151
  },
154
152
 
155
153
  /** Reveal a secret's plaintext value using a single-use challenge from `revealChallenge()`.
156
- * `GET .../<name>?region=<region>` with `X-K9O-Reveal-Challenge`. Decodes the wire's base64
157
- * `value` back to plaintext (live-verified: the reveal response's `value` is base64, symmetric
158
- * with `create`'s encode). */
159
- async reveal(name: string, opts: { region: string; challenge: string }): Promise<Envelope<RevealSecretResult>> {
154
+ * `GET .../<name>` with `X-K9O-Reveal-Challenge`. Decodes the wire's base64 `value` back to plaintext
155
+ * (symmetric with `create`'s encode). */
156
+ async reveal(name: string, opts: { region?: string; challenge: string }): Promise<Envelope<RevealSecretResult>> {
160
157
  const envelope = await request<RevealSecretResult>(config, {
161
158
  method: "GET",
162
159
  path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
@@ -166,9 +163,8 @@ export function makeVaultNamespace(config: HttpClientConfig) {
166
163
  return { ...envelope, data: { ...envelope.data, value: decodeBase64Utf8(envelope.data.value) } };
167
164
  },
168
165
 
169
- /** Update a secret's value (bumps version) — owner-only. `PUT .../<name>?region=<region>`.
170
- * 🔴 Returns only `{id, name, version}` live (see `UpdateSecretResult`), not the full summary
171
- * the OpenAPI schema documents — call `get()` after if you need the rest. */
166
+ /** Update a secret's value (bumps version), owner-only. `PUT .../<name>`.
167
+ * Returns only `{id, name, version}` (see `UpdateSecretResult`); call `get()` after for the rest. */
172
168
  async update(name: string, input: UpdateSecretInput): Promise<Envelope<UpdateSecretResult>> {
173
169
  return request<UpdateSecretResult>(config, {
174
170
  method: "PUT",
@@ -178,19 +174,18 @@ export function makeVaultNamespace(config: HttpClientConfig) {
178
174
  });
179
175
  },
180
176
 
181
- /** Rotate a secret's wrapping key (re-wraps under a fresh `kid`; content unchanged) — owner-only.
182
- * `POST .../<name>?op=rotate&region=<region>`. */
183
- async rotate(name: string, opts: { region: string }): Promise<Envelope<RotateSecretResult>> {
177
+ /** Rotate a secret's wrapping key (re-wraps under a fresh `kid`; content unchanged), owner-only.
178
+ * `POST .../<name>?op=rotate`. */
179
+ async rotate(name: string, opts?: { region?: string }): Promise<Envelope<RotateSecretResult>> {
184
180
  return request<RotateSecretResult>(config, {
185
181
  method: "POST",
186
182
  path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
187
- query: { op: "rotate", region: opts.region },
183
+ query: { op: "rotate", region: opts?.region },
188
184
  });
189
185
  },
190
186
 
191
- /** Move (rename) a secret's hierarchical path — owner-only.
192
- * `POST .../<name>?op=move&region=<region>`. */
193
- async move(name: string, opts: { region: string; newPath: string }): Promise<Envelope<MoveSecretResult>> {
187
+ /** Move (rename) a secret's hierarchical path, owner-only. `POST .../<name>?op=move`. */
188
+ async move(name: string, opts: { region?: string; newPath: string }): Promise<Envelope<MoveSecretResult>> {
194
189
  return request<MoveSecretResult>(config, {
195
190
  method: "POST",
196
191
  path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
@@ -199,12 +194,12 @@ export function makeVaultNamespace(config: HttpClientConfig) {
199
194
  });
200
195
  },
201
196
 
202
- /** Retire a secret — owner-only. `DELETE .../<name>?region=<region>` → `204 No Content`. */
203
- async delete(name: string, opts: { region: string }): Promise<Envelope<void>> {
197
+ /** Retire a secret, owner-only. `DELETE .../<name>`. */
198
+ async delete(name: string, opts?: { region?: string }): Promise<Envelope<void>> {
204
199
  return request<void>(config, {
205
200
  method: "DELETE",
206
201
  path: projectPath(requireProjectSlug(config), `vault/secrets/${encodeURIComponent(name)}`),
207
- query: { region: opts.region },
202
+ query: { region: opts?.region },
208
203
  });
209
204
  },
210
205
  },
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Loads the nodejs-target glue that wasm-bindgen emits for the bound core.
3
+ *
4
+ * That glue is CommonJS (`exports.zk_encrypt = ...`, `require("fs")`, a synchronous read of the sibling `_bg.wasm`).
5
+ * This package is ESM (`"type": "module"`), so Node parses any `.js` under it as ESM and the glue dies with
6
+ * `exports is not defined`; `import()` and `require()` of that file both hit it. The default loader therefore reads the
7
+ * glue source and evaluates it with a CommonJS wrapper itself, which does not depend on the file extension or on the
8
+ * nearest `package.json`.
9
+ *
10
+ * Failures are reported as what they are: no glue file at all, or a glue file that is there and failed (with the
11
+ * underlying error attached as `cause`).
12
+ */
13
+
14
+ import { KError } from "@k9o/core/errors";
15
+ import type { ZkWasm } from "./crypto.ts";
16
+
17
+ /** What the loader needs from the host; injectable so the failure paths are testable without a wasm artefact. */
18
+ export interface WasmLoaderDeps {
19
+ /** Absolute paths to try, in order. */
20
+ candidates: string[];
21
+ /** Load the glue at `path`. Must reject with an error whose `code` is `ENOENT`/`MODULE_NOT_FOUND`/`ERR_MODULE_NOT_FOUND`
22
+ * and whose `path` (or message) names `path` when that file is missing. */
23
+ importGlue(path: string): Promise<unknown>;
24
+ }
25
+
26
+ const NOT_FOUND_CODES = new Set(["ENOENT", "MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND"]);
27
+
28
+ function describe(e: unknown): string {
29
+ return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
30
+ }
31
+
32
+ /** True when `e` says the file `path` itself is missing (not a file the glue went on to read). */
33
+ function isMissing(e: unknown, path: string): boolean {
34
+ if (typeof e !== "object" || e === null) return false;
35
+ const err = e as { code?: unknown; path?: unknown; message?: unknown };
36
+ if (typeof err.code !== "string" || !NOT_FOUND_CODES.has(err.code)) return false;
37
+ if (typeof err.path === "string") return err.path === path;
38
+ return typeof err.message === "string" && err.message.includes(path);
39
+ }
40
+
41
+ function withCause(err: KError, cause: unknown): KError {
42
+ (err as { cause?: unknown }).cause = cause;
43
+ return err;
44
+ }
45
+
46
+ export async function loadZkGlue(deps: WasmLoaderDeps): Promise<ZkWasm> {
47
+ for (const path of deps.candidates) {
48
+ let mod: unknown;
49
+ try {
50
+ mod = await deps.importGlue(path);
51
+ } catch (e) {
52
+ if (isMissing(e, path)) continue;
53
+ throw withCause(
54
+ new KError({
55
+ code: "KIBO_WASM_ERROR",
56
+ message: `the Kibo zero-knowledge core (wasm) is present at ${path} but failed to load: ${describe(e)}`,
57
+ context: { path, cause: describe(e) },
58
+ recommendation: "Reinstall @k9o/kibo; if it persists, load the core yourself and pass it to setZkWasm.",
59
+ }),
60
+ e,
61
+ );
62
+ }
63
+ const m = mod as Partial<ZkWasm> | null | undefined;
64
+ if (typeof m?.zk_encrypt !== "function" || typeof m?.zk_decrypt !== "function") {
65
+ throw new KError({
66
+ code: "KIBO_WASM_ERROR",
67
+ message: `the Kibo zero-knowledge core (wasm) loaded from ${path} does not export zk_encrypt and zk_decrypt`,
68
+ context: { path },
69
+ recommendation: "Reinstall @k9o/kibo; the bundled core does not match this version.",
70
+ });
71
+ }
72
+ return m as ZkWasm;
73
+ }
74
+ throw new KError({
75
+ code: "KIBO_WASM_ERROR",
76
+ message: `the Kibo zero-knowledge core (wasm) is not bundled in this build of @k9o/kibo: no file at ${deps.candidates.join(" or ")}; encrypt and decrypt are unavailable`,
77
+ context: { candidates: deps.candidates },
78
+ recommendation: "Install a release of @k9o/kibo that ships the core, or load it yourself and pass it to setZkWasm.",
79
+ });
80
+ }
81
+
82
+ /** Evaluate CommonJS source as a module living at `path`, with a `require` resolving from `path`. */
83
+ export function evalCommonJs(source: string, path: string, req: (id: string) => unknown, dirname: string): unknown {
84
+ const module = { exports: {} as Record<string, unknown> };
85
+ // eslint-disable-next-line no-new-func
86
+ const wrapper = new Function("exports", "require", "module", "__filename", "__dirname", source);
87
+ wrapper(module.exports, req, module, path, dirname);
88
+ return module.exports;
89
+ }
90
+
91
+ /** Node host: glue next to this package's `wasm/` directory, evaluated as CommonJS. */
92
+ export function nodeDeps(moduleUrl: string): WasmLoaderDeps {
93
+ // The node builtins are named at run time so a browser/RN bundler never tries to resolve them: this path only runs on Node.
94
+ const builtin = (n: string) => ["node", n].join(":");
95
+ const here = (rel: string) => decodeURIComponent(new URL(rel, moduleUrl).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
96
+ return {
97
+ candidates: [here("../wasm/kibo_kms_rest_ffi.cjs"), here("../wasm/kibo_kms_rest_ffi.js")],
98
+ async importGlue(path: string) {
99
+ const fs = (await import(builtin("fs"))) as typeof import("node:fs");
100
+ const nodeModule = (await import(builtin("module"))) as typeof import("node:module");
101
+ const nodePath = (await import(builtin("path"))) as typeof import("node:path");
102
+ const source = fs.readFileSync(path, "utf8"); // throws ENOENT with err.path === path when the glue is missing
103
+ return evalCommonJs(source, path, nodeModule.createRequire(path), nodePath.dirname(path));
104
+ },
105
+ };
106
+ }
@@ -417,7 +417,7 @@ function __wbg_get_imports() {
417
417
  const a = state0.a;
418
418
  state0.a = 0;
419
419
  try {
420
- return __wasm_bindgen_func_elem_722(a, state0.b, arg0, arg1);
420
+ return __wasm_bindgen_func_elem_721(a, state0.b, arg0, arg1);
421
421
  } finally {
422
422
  state0.a = a;
423
423
  }
@@ -575,12 +575,12 @@ function __wbg_get_imports() {
575
575
  },
576
576
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
577
577
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 39, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
578
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_441);
578
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_440);
579
579
  return addHeapObject(ret);
580
580
  },
581
581
  __wbindgen_cast_0000000000000002: function(arg0, arg1) {
582
582
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 73, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
583
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1519);
583
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1520);
584
584
  return addHeapObject(ret);
585
585
  },
586
586
  __wbindgen_cast_0000000000000003: function(arg0) {
@@ -617,14 +617,14 @@ function __wbg_get_imports() {
617
617
  };
618
618
  }
619
619
 
620
- function __wasm_bindgen_func_elem_1519(arg0, arg1) {
621
- wasm.__wasm_bindgen_func_elem_1519(arg0, arg1);
620
+ function __wasm_bindgen_func_elem_1520(arg0, arg1) {
621
+ wasm.__wasm_bindgen_func_elem_1520(arg0, arg1);
622
622
  }
623
623
 
624
- function __wasm_bindgen_func_elem_441(arg0, arg1, arg2) {
624
+ function __wasm_bindgen_func_elem_440(arg0, arg1, arg2) {
625
625
  try {
626
626
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
627
- wasm.__wasm_bindgen_func_elem_441(retptr, arg0, arg1, addHeapObject(arg2));
627
+ wasm.__wasm_bindgen_func_elem_440(retptr, arg0, arg1, addHeapObject(arg2));
628
628
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
629
629
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
630
630
  if (r1) {
@@ -635,8 +635,8 @@ function __wasm_bindgen_func_elem_441(arg0, arg1, arg2) {
635
635
  }
636
636
  }
637
637
 
638
- function __wasm_bindgen_func_elem_722(arg0, arg1, arg2, arg3) {
639
- wasm.__wasm_bindgen_func_elem_722(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
638
+ function __wasm_bindgen_func_elem_721(arg0, arg1, arg2, arg3) {
639
+ wasm.__wasm_bindgen_func_elem_721(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
640
640
  }
641
641
 
642
642
 
Binary file