@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/CHANGELOG.md +23 -0
- package/README.md +53 -33
- package/dist/index.d.ts +54 -68
- package/dist/index.js +133 -75
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +18 -31
- package/package.json +3 -3
- package/src/client-factory.ts +1 -1
- package/src/crypto-types.ts +3 -3
- package/src/crypto.ts +11 -24
- package/src/index.ts +8 -13
- package/src/types.ts +18 -31
- package/src/vault.ts +71 -76
- package/src/wasm-loader.ts +106 -0
- package/wasm/kibo_kms_rest_ffi.js +9 -9
- package/wasm/kibo_kms_rest_ffi_bg.wasm +0 -0
- package/wasm/kibo_kms_rest_ffi_bg.wasm.d.ts +3 -3
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
|
|
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
|
-
*
|
|
13
|
-
* `vault.zkDeks.{store,fetch}` surface
|
|
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
|
-
* `
|
|
45
|
-
* `
|
|
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
|
-
|
|
53
|
-
|
|
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`
|
|
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`
|
|
2
|
+
* `@k9o/kibo` - Zero-Knowledge BYOK/HYOK vault SDK.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
|
130
|
-
|
|
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
|
|
139
|
-
*
|
|
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
|
|
151
|
-
*
|
|
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
|
|
164
|
-
*
|
|
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.*`
|
|
2
|
+
* `client.kibo.vault.*` - the Kibo vault surface.
|
|
3
3
|
*
|
|
4
|
-
* `vault.zkDeks.{store,fetch}`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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`)
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
|
86
|
-
*
|
|
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
|
|
91
|
-
|
|
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
|
|
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
|
-
*
|
|
105
|
-
* `
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
//
|
|
138
|
-
//
|
|
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
|
|
146
|
-
*
|
|
147
|
-
async revealChallenge(name: string, opts
|
|
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
|
|
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
|
|
157
|
-
*
|
|
158
|
-
|
|
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)
|
|
170
|
-
*
|
|
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)
|
|
182
|
-
* `POST .../<name>?op=rotate
|
|
183
|
-
async rotate(name: string, opts
|
|
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
|
|
183
|
+
query: { op: "rotate", region: opts?.region },
|
|
188
184
|
});
|
|
189
185
|
},
|
|
190
186
|
|
|
191
|
-
/** Move (rename) a secret's hierarchical path
|
|
192
|
-
|
|
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
|
|
203
|
-
async delete(name: string, opts
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
621
|
-
wasm.
|
|
620
|
+
function __wasm_bindgen_func_elem_1520(arg0, arg1) {
|
|
621
|
+
wasm.__wasm_bindgen_func_elem_1520(arg0, arg1);
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
-
function
|
|
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.
|
|
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
|
|
639
|
-
wasm.
|
|
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
|