@kitsy/cnos 1.1.0 → 1.1.2

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
@@ -2,4 +2,18 @@
2
2
 
3
3
  Developer-friendly CNOS runtime assembly. It bundles the core engine plus the official built-in plugins, exposes the main `createCnos(...)` entry point for app code, and re-exports the built-ins under `@kitsy/cnos/plugins/*`.
4
4
 
5
+ Current runtime surface includes:
6
+ - `createCnos()`
7
+ - `read`, `require`, `readOr`
8
+ - `value`, `secret`, `meta`
9
+ - `inspect`
10
+ - `toObject`, `toNamespace`
11
+ - `toEnv`, `toPublicEnv`
12
+
13
+ CLI-oriented storage/export rules to be aware of:
14
+ - user-defined values and secrets remain private by default
15
+ - public/browser exposure comes from `public.promote`
16
+ - shell env export comes from explicit `envMapping.explicit`
17
+ - local secret material lives outside the repo in encrypted vault storage under `~/.cnos/secrets`
18
+
5
19
  Use `@kitsy/cnos-vite` for Vite projects and `@kitsy/cnos-next` for Next.js projects when you want CNOS public values projected into framework-native env surfaces.
@@ -120,6 +120,151 @@ function stringifyYaml(value) {
120
120
  return stringify(value);
121
121
  }
122
122
 
123
+ // ../core/src/utils/secretStore.ts
124
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
125
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
126
+ import path2 from "path";
127
+ function isObject(value) {
128
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
129
+ }
130
+ function isSecretReference(value) {
131
+ return isObject(value) && typeof value.provider === "string" && value.provider.trim().length > 0 && typeof value.ref === "string" && value.ref.trim().length > 0 && (value.vault === void 0 && true || typeof value.vault === "string" && value.vault.trim().length > 0) && Object.keys(value).every((key) => ["provider", "ref", "vault"].includes(key));
132
+ }
133
+ function resolveSecretStoreRoot(processEnv = process.env) {
134
+ return path2.resolve(expandHomePath(processEnv.CNOS_SECRET_HOME ?? "~/.cnos/secrets"));
135
+ }
136
+ function resolveSecretVaultFile(storeRoot, vault = "default") {
137
+ return path2.join(storeRoot, "vaults", `${vault}.json`);
138
+ }
139
+ function resolveSecretStoreFile(storeRoot, ref, vault = "default") {
140
+ return path2.join(storeRoot, "vaults", vault, "store", ...ref.split("/")).concat(".json");
141
+ }
142
+ function deriveKey(passphrase, salt) {
143
+ return scryptSync(passphrase, salt, 32);
144
+ }
145
+ function resolveSecretPassphrase(vault = "default", processEnv = process.env) {
146
+ const vaultToken = vault.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
147
+ return processEnv[`CNOS_SECRET_PASSPHRASE_${vaultToken}`] ?? processEnv.CNOS_SECRET_PASSPHRASE;
148
+ }
149
+ function encryptDocument(value, passphrase) {
150
+ const salt = randomBytes(16);
151
+ const iv = randomBytes(12);
152
+ const key = deriveKey(passphrase, salt);
153
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
154
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
155
+ const tag = cipher.getAuthTag();
156
+ return {
157
+ version: 1,
158
+ algorithm: "aes-256-gcm",
159
+ salt: salt.toString("base64"),
160
+ iv: iv.toString("base64"),
161
+ tag: tag.toString("base64"),
162
+ ciphertext: ciphertext.toString("base64")
163
+ };
164
+ }
165
+ function decryptDocument(document, passphrase) {
166
+ const salt = Buffer.from(document.salt, "base64");
167
+ const iv = Buffer.from(document.iv, "base64");
168
+ const tag = Buffer.from(document.tag, "base64");
169
+ const ciphertext = Buffer.from(document.ciphertext, "base64");
170
+ const key = deriveKey(passphrase, salt);
171
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
172
+ decipher.setAuthTag(tag);
173
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
174
+ return plaintext.toString("utf8");
175
+ }
176
+ async function createSecretVault(storeRoot, vault, passphrase) {
177
+ const normalizedVault = vault.trim() || "default";
178
+ const filePath = resolveSecretVaultFile(storeRoot, normalizedVault);
179
+ await mkdir(path2.dirname(filePath), { recursive: true });
180
+ const document = {
181
+ version: 1,
182
+ name: normalizedVault,
183
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
184
+ verifier: encryptDocument(`cnos-vault:${normalizedVault}`, passphrase)
185
+ };
186
+ await writeFile(filePath, JSON.stringify(document, null, 2), "utf8");
187
+ return filePath;
188
+ }
189
+ async function ensureSecretVault(storeRoot, vault, passphrase) {
190
+ const normalizedVault = vault.trim() || "default";
191
+ const filePath = resolveSecretVaultFile(storeRoot, normalizedVault);
192
+ try {
193
+ await readFile(filePath, "utf8");
194
+ return filePath;
195
+ } catch (error) {
196
+ if (error.code !== "ENOENT") {
197
+ throw error;
198
+ }
199
+ }
200
+ return createSecretVault(storeRoot, normalizedVault, passphrase);
201
+ }
202
+ async function listSecretVaults(storeRoot) {
203
+ const vaultRoot = path2.join(storeRoot, "vaults");
204
+ try {
205
+ const entries = await readdir(vaultRoot, { withFileTypes: true });
206
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.replace(/\.json$/, "")).sort((left, right) => left.localeCompare(right));
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+ async function writeLocalSecret(storeRoot, ref, value, passphrase, vault = "default") {
212
+ await ensureSecretVault(storeRoot, vault, passphrase);
213
+ const filePath = resolveSecretStoreFile(storeRoot, ref, vault);
214
+ await mkdir(path2.dirname(filePath), { recursive: true });
215
+ await writeFile(filePath, JSON.stringify(encryptDocument(value, passphrase), null, 2), "utf8");
216
+ return filePath;
217
+ }
218
+ async function readLocalSecret(storeRoot, ref, passphrase, vault = "default") {
219
+ if (!passphrase) {
220
+ throw new CnosManifestError(
221
+ `Missing CNOS secret passphrase for local secret ref "${ref}". Set CNOS_SECRET_PASSPHRASE or pass processEnv explicitly.`
222
+ );
223
+ }
224
+ const filePath = resolveSecretStoreFile(storeRoot, ref, vault);
225
+ const source = await readFile(filePath, "utf8");
226
+ const document = JSON.parse(source);
227
+ if (document.version !== 1 || document.algorithm !== "aes-256-gcm" || typeof document.salt !== "string" || typeof document.iv !== "string" || typeof document.tag !== "string" || typeof document.ciphertext !== "string") {
228
+ throw new CnosManifestError("Invalid local secret document", filePath);
229
+ }
230
+ return decryptDocument(document, passphrase);
231
+ }
232
+
233
+ // ../core/src/runtime/toEnv.ts
234
+ function normalizeEnvValue(value) {
235
+ if (value === void 0 || value === null) {
236
+ return "";
237
+ }
238
+ if (typeof value === "string") {
239
+ return value;
240
+ }
241
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
242
+ return String(value);
243
+ }
244
+ return JSON.stringify(value);
245
+ }
246
+ function toEnv(graph, manifest, options = {}) {
247
+ const includeSecrets = options.includeSecrets ?? true;
248
+ const output = {};
249
+ const mappedEntries = Object.entries(manifest.envMapping.explicit).sort(
250
+ ([left], [right]) => left.localeCompare(right)
251
+ );
252
+ for (const [envVar, logicalKey] of mappedEntries) {
253
+ const entry = graph.entries.get(logicalKey);
254
+ if (!entry) {
255
+ continue;
256
+ }
257
+ if (entry.namespace === "secret" && !includeSecrets) {
258
+ continue;
259
+ }
260
+ if (isSecretReference(entry.value)) {
261
+ continue;
262
+ }
263
+ output[envVar] = normalizeEnvValue(entry.value);
264
+ }
265
+ return output;
266
+ }
267
+
123
268
  // ../core/src/utils/envNaming.ts
124
269
  function normalizeMappingConfig(config = {}) {
125
270
  return {
@@ -175,48 +320,6 @@ function envVarToLogicalKey(envVar, config = {}) {
175
320
  return `value.${fromScreamingSnake(envVar)}`;
176
321
  }
177
322
 
178
- // ../core/src/runtime/toEnv.ts
179
- function fallbackLogicalKeyToEnvVar(key) {
180
- if (key.startsWith("value.")) {
181
- return key.slice("value.".length).replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
182
- }
183
- if (key.startsWith("secret.")) {
184
- const normalized = key.slice("secret.".length).replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
185
- return `SECRET_${normalized}`;
186
- }
187
- return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
188
- }
189
- function normalizeEnvValue(value) {
190
- if (value === void 0 || value === null) {
191
- return "";
192
- }
193
- if (typeof value === "string") {
194
- return value;
195
- }
196
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
197
- return String(value);
198
- }
199
- return JSON.stringify(value);
200
- }
201
- function toEnv(graph, manifest, options = {}) {
202
- const includeSecrets = options.includeSecrets ?? true;
203
- const output = {};
204
- const resolvedEntries = Array.from(graph.entries.values()).sort(
205
- (left, right) => left.key.localeCompare(right.key)
206
- );
207
- for (const entry of resolvedEntries) {
208
- if (entry.namespace === "meta") {
209
- continue;
210
- }
211
- if (!includeSecrets && entry.namespace === "secret") {
212
- continue;
213
- }
214
- const envVar = logicalKeyToEnvVar(entry.key, manifest.envMapping) ?? fallbackLogicalKeyToEnvVar(entry.key);
215
- output[envVar] = normalizeEnvValue(entry.value);
216
- }
217
- return output;
218
- }
219
-
220
323
  // ../core/src/runtime/toPublicEnv.ts
221
324
  function fallbackValueEnvVar(key) {
222
325
  return key.replace(/^value\./, "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
@@ -269,8 +372,8 @@ function toPublicEnv(graph, manifest, options = {}) {
269
372
  }
270
373
 
271
374
  // ../core/src/runtime/dump.ts
272
- import { mkdir, writeFile } from "fs/promises";
273
- import path2 from "path";
375
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
376
+ import path3 from "path";
274
377
 
275
378
  // ../core/src/runtime/projection.ts
276
379
  function setNestedValue(target, pathSegments, value) {
@@ -304,20 +407,20 @@ function toNamespaceObject(graph, namespace) {
304
407
 
305
408
  // ../core/src/runtime/dump.ts
306
409
  function buildDumpFiles(graph, options = {}) {
307
- const basePath = options.flatten ? "" : path2.posix.join("workspaces", graph.workspace.workspaceId);
410
+ const basePath = options.flatten ? "" : path3.posix.join("workspaces", graph.workspace.workspaceId);
308
411
  const values = toNamespaceObject(graph, "value");
309
412
  const secrets = toNamespaceObject(graph, "secret");
310
413
  const files = [];
311
414
  if (Object.keys(values).length > 0) {
312
415
  files.push({
313
- path: path2.posix.join(basePath, "values", graph.profile, "app.yml"),
416
+ path: path3.posix.join(basePath, "values", graph.profile, "app.yml"),
314
417
  namespace: "value",
315
418
  content: stringifyYaml(values)
316
419
  });
317
420
  }
318
421
  if (Object.keys(secrets).length > 0) {
319
422
  files.push({
320
- path: path2.posix.join(basePath, "secrets", graph.profile, "app.yml"),
423
+ path: path3.posix.join(basePath, "secrets", graph.profile, "app.yml"),
321
424
  namespace: "secret",
322
425
  content: stringifyYaml(secrets)
323
426
  });
@@ -333,12 +436,12 @@ function planDump(graph, options = {}) {
333
436
  };
334
437
  }
335
438
  async function writeDump(graph, options) {
336
- const root = path2.resolve(options.to);
439
+ const root = path3.resolve(options.to);
337
440
  const plan = planDump(graph, options);
338
441
  for (const file of plan.files) {
339
- const destination = path2.join(root, file.path);
340
- await mkdir(path2.dirname(destination), { recursive: true });
341
- await writeFile(destination, file.content, "utf8");
442
+ const destination = path3.join(root, file.path);
443
+ await mkdir2(path3.dirname(destination), { recursive: true });
444
+ await writeFile2(destination, file.content, "utf8");
342
445
  }
343
446
  return {
344
447
  ...plan,
@@ -359,75 +462,8 @@ function flattenObject(value, prefix = "") {
359
462
  }, {});
360
463
  }
361
464
 
362
- // ../core/src/utils/secretStore.ts
363
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
364
- import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
365
- import path3 from "path";
366
- function isObject(value) {
367
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
368
- }
369
- function isSecretReference(value) {
370
- return isObject(value) && typeof value.provider === "string" && value.provider.trim().length > 0 && typeof value.ref === "string" && value.ref.trim().length > 0 && Object.keys(value).every((key) => ["provider", "ref"].includes(key));
371
- }
372
- function resolveSecretStoreRoot(processEnv = process.env) {
373
- return path3.resolve(expandHomePath(processEnv.CNOS_SECRET_HOME ?? "~/.cnos/secrets"));
374
- }
375
- function resolveSecretStoreFile(storeRoot, ref) {
376
- return path3.join(storeRoot, "store", ...ref.split("/")).concat(".json");
377
- }
378
- function deriveKey(passphrase, salt) {
379
- return scryptSync(passphrase, salt, 32);
380
- }
381
- function encryptDocument(value, passphrase) {
382
- const salt = randomBytes(16);
383
- const iv = randomBytes(12);
384
- const key = deriveKey(passphrase, salt);
385
- const cipher = createCipheriv("aes-256-gcm", key, iv);
386
- const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
387
- const tag = cipher.getAuthTag();
388
- return {
389
- version: 1,
390
- algorithm: "aes-256-gcm",
391
- salt: salt.toString("base64"),
392
- iv: iv.toString("base64"),
393
- tag: tag.toString("base64"),
394
- ciphertext: ciphertext.toString("base64")
395
- };
396
- }
397
- function decryptDocument(document, passphrase) {
398
- const salt = Buffer.from(document.salt, "base64");
399
- const iv = Buffer.from(document.iv, "base64");
400
- const tag = Buffer.from(document.tag, "base64");
401
- const ciphertext = Buffer.from(document.ciphertext, "base64");
402
- const key = deriveKey(passphrase, salt);
403
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
404
- decipher.setAuthTag(tag);
405
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
406
- return plaintext.toString("utf8");
407
- }
408
- async function writeLocalSecret(storeRoot, ref, value, passphrase) {
409
- const filePath = resolveSecretStoreFile(storeRoot, ref);
410
- await mkdir2(path3.dirname(filePath), { recursive: true });
411
- await writeFile2(filePath, JSON.stringify(encryptDocument(value, passphrase), null, 2), "utf8");
412
- return filePath;
413
- }
414
- async function readLocalSecret(storeRoot, ref, passphrase) {
415
- if (!passphrase) {
416
- throw new CnosManifestError(
417
- `Missing CNOS secret passphrase for local secret ref "${ref}". Set CNOS_SECRET_PASSPHRASE or pass processEnv explicitly.`
418
- );
419
- }
420
- const filePath = resolveSecretStoreFile(storeRoot, ref);
421
- const source = await readFile(filePath, "utf8");
422
- const document = JSON.parse(source);
423
- if (document.version !== 1 || document.algorithm !== "aes-256-gcm" || typeof document.salt !== "string" || typeof document.iv !== "string" || typeof document.tag !== "string" || typeof document.ciphertext !== "string") {
424
- throw new CnosManifestError("Invalid local secret document", filePath);
425
- }
426
- return decryptDocument(document, passphrase);
427
- }
428
-
429
465
  // ../core/src/validation/envMapping.ts
430
- function fallbackLogicalKeyToEnvVar2(key) {
466
+ function fallbackLogicalKeyToEnvVar(key) {
431
467
  return key.replace(/^(value|secret)\./, "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
432
468
  }
433
469
  function validateEnvMappingCollisions(manifest, graph) {
@@ -442,7 +478,7 @@ function validateEnvMappingCollisions(manifest, graph) {
442
478
  if (key.startsWith("meta.")) {
443
479
  continue;
444
480
  }
445
- const envVar = logicalKeyToEnvVar(key, manifest.envMapping) ?? (key.startsWith("value.") || key.startsWith("secret.") ? fallbackLogicalKeyToEnvVar2(key) : void 0);
481
+ const envVar = logicalKeyToEnvVar(key, manifest.envMapping) ?? (key.startsWith("value.") || key.startsWith("secret.") ? fallbackLogicalKeyToEnvVar(key) : void 0);
446
482
  if (!envVar) {
447
483
  continue;
448
484
  }
@@ -1550,16 +1586,20 @@ export {
1550
1586
  parseYaml,
1551
1587
  stringifyYaml,
1552
1588
  applySchemaRules,
1553
- envVarToLogicalKey,
1589
+ isSecretReference,
1590
+ resolveSecretStoreRoot,
1591
+ resolveSecretVaultFile,
1592
+ resolveSecretPassphrase,
1593
+ createSecretVault,
1594
+ listSecretVaults,
1595
+ writeLocalSecret,
1596
+ readLocalSecret,
1554
1597
  toEnv,
1598
+ envVarToLogicalKey,
1555
1599
  toPublicEnv,
1556
1600
  createCnos,
1557
1601
  planDump,
1558
1602
  writeDump,
1559
1603
  flattenObject,
1560
- isSecretReference,
1561
- resolveSecretStoreRoot,
1562
- writeLocalSecret,
1563
- readLocalSecret,
1564
1604
  validateRuntime
1565
1605
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  envVarToLogicalKey
3
- } from "./chunk-K2T4R5WH.js";
3
+ } from "./chunk-33ZDYDQJ.js";
4
4
 
5
5
  // ../../plugins/process-env/src/index.ts
6
6
  var PROCESS_ENV_PLUGIN_ID = "@kitsy/cnos/plugins/process-env";
@@ -3,9 +3,10 @@ import {
3
3
  isSecretReference,
4
4
  parseYaml,
5
5
  readLocalSecret,
6
+ resolveSecretPassphrase,
6
7
  resolveSecretStoreRoot,
7
8
  toPortablePath
8
- } from "./chunk-K2T4R5WH.js";
9
+ } from "./chunk-33ZDYDQJ.js";
9
10
 
10
11
  // ../../plugins/filesystem/src/helpers.ts
11
12
  import { readdir } from "fs/promises";
@@ -102,13 +103,15 @@ async function resolveSecretValue(value, processEnv) {
102
103
  return value;
103
104
  }
104
105
  if (value.provider === "local") {
105
- if (!processEnv?.CNOS_SECRET_PASSPHRASE) {
106
+ const passphrase = resolveSecretPassphrase(value.vault, processEnv);
107
+ if (!passphrase) {
106
108
  return value;
107
109
  }
108
110
  return readLocalSecret(
109
111
  resolveSecretStoreRoot(processEnv),
110
112
  value.ref,
111
- processEnv?.CNOS_SECRET_PASSPHRASE
113
+ passphrase,
114
+ value.vault
112
115
  );
113
116
  }
114
117
  if (value.provider === "env") {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  joinConfigPath
3
- } from "./chunk-K2T4R5WH.js";
3
+ } from "./chunk-33ZDYDQJ.js";
4
4
 
5
5
  // ../../plugins/cli-args/src/index.ts
6
6
  var CLI_ARGS_PLUGIN_ID = "@kitsy/cnos/plugins/cli-args";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  toEnv,
3
3
  toPublicEnv
4
- } from "./chunk-K2T4R5WH.js";
4
+ } from "./chunk-33ZDYDQJ.js";
5
5
 
6
6
  // ../../plugins/env-export/src/index.ts
7
7
  function createEnvExportPlugin() {
@@ -2,7 +2,7 @@ import {
2
2
  envVarToLogicalKey,
3
3
  resolveWorkspaceScopedPath,
4
4
  toPortablePath
5
- } from "./chunk-K2T4R5WH.js";
5
+ } from "./chunk-33ZDYDQJ.js";
6
6
 
7
7
  // ../../plugins/dotenv/src/index.ts
8
8
  import { readFile } from "fs/promises";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  applySchemaRules
3
- } from "./chunk-K2T4R5WH.js";
3
+ } from "./chunk-33ZDYDQJ.js";
4
4
 
5
5
  // ../../plugins/basic-schema/src/index.ts
6
6
  function createBasicSchemaPlugin() {