@cotal-ai/workspace 0.11.6 → 0.12.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.
@@ -0,0 +1,80 @@
1
+ import { readFileSync, rmSync } from "node:fs";
2
+ import { join, dirname, isAbsolute, normalize, relative, resolve, sep } from "node:path";
3
+ import { mkSecretDir, writeSecretFileAtomic } from "@cotal-ai/core";
4
+ /** THE local composition of the secret keyspace: a filesystem store rooted at the workspace's
5
+ * `.cotal/` dir, so every canonical key (`delivery.creds`, `auth/<space>/callout.json`, …)
6
+ * lands byte-for-byte on today's paths. Every local caller composes through here — a hand-rolled
7
+ * root that drifted from `.cotal` would silently split the keyspace in two. */
8
+ export function workspaceSecretStore(root) {
9
+ return new FsSecretStore(join(root, ".cotal"));
10
+ }
11
+ /**
12
+ * The default filesystem {@link SecretStore}: the workstation adapter behind core's abstract seam.
13
+ *
14
+ * Keys are opaque logical strings the owning packages build to MIRROR today's exact `.cotal` layout
15
+ * per kind — `delivery.creds`, `auth/callout.json`, `<agent>.creds`, and so on — so a local
16
+ * `cotal up` writes byte-for-byte the same files it does today. That layout is heterogeneous (a few
17
+ * kinds are already space-segmented on disk, most are not), so each key builder reproduces its
18
+ * kind's current path verbatim. Crucially the key carries NO injected tenant / `<space>` prefix: a
19
+ * hosted KMS/Vault adapter adds per-tenant scope inside ITS OWN resolve from its injection context,
20
+ * which keeps one backend-agnostic key per kind and preserves local byte-for-byte.
21
+ *
22
+ * This adapter adds nothing of its own: it delegates to the `secret-fs` primitives for the
23
+ * 0600 / icacls / atomic-replace / fail-closed guarantees. Constructed with an EXPLICIT base root
24
+ * (no ambient `~/.cotal`), absolutized so keys are cwd-independent. Lives in `@cotal-ai/workspace`,
25
+ * not core, because on-disk layout is a workstation concern, not the wire protocol.
26
+ *
27
+ * Residual (documented): a symlink UNDER the root that points outside is not caught (no realpath) —
28
+ * acceptable for a single-tenant workstation; a multi-tenant FS deployment should realpath-check.
29
+ */
30
+ export class FsSecretStore {
31
+ root;
32
+ constructor(root) {
33
+ if (!root)
34
+ throw new Error("FsSecretStore: root is required");
35
+ this.root = normalize(resolve(root)); // absolutize so keys are cwd-independent
36
+ }
37
+ /** Resolve a logical key to an absolute path strictly UNDER `root`, fail-closed: reject empty,
38
+ * NUL, absolute keys, the root itself (`.`), and any key that normalizes outside the root
39
+ * (`..` traversal). Containment is checked via `path.relative`, not a `root + sep` prefix —
40
+ * the prefix form breaks at a filesystem-root base (`/` doubles the separator and rejects
41
+ * every key). A malformed key must never read or clobber a path outside the store's tree. */
42
+ resolve(key) {
43
+ if (!key || key.includes("\0"))
44
+ throw new Error(`FsSecretStore: invalid key ${JSON.stringify(key)}`);
45
+ if (isAbsolute(key))
46
+ throw new Error(`FsSecretStore: key must be relative, got ${JSON.stringify(key)}`);
47
+ const abs = normalize(join(this.root, key));
48
+ const rel = relative(this.root, abs);
49
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
50
+ throw new Error(`FsSecretStore: key must name a file under the root: ${JSON.stringify(key)}`);
51
+ return abs;
52
+ }
53
+ async get(key) {
54
+ const p = this.resolve(key);
55
+ try {
56
+ return readFileSync(p, "utf8");
57
+ }
58
+ catch (e) {
59
+ if (e.code === "ENOENT")
60
+ return undefined; // absent → undefined, race-safe
61
+ throw e;
62
+ }
63
+ }
64
+ async put(key, value) {
65
+ const p = this.resolve(key);
66
+ mkSecretDir(dirname(p)); // harden the parent dir before the secret lands (0700 / private ACL)
67
+ writeSecretFileAtomic(p, value); // temp + rename: a concurrent get sees old or new, never torn
68
+ }
69
+ async delete(key) {
70
+ const p = this.resolve(key);
71
+ try {
72
+ rmSync(p);
73
+ }
74
+ catch (e) {
75
+ if (e.code !== "ENOENT")
76
+ throw e; // already-absent is success (idempotent)
77
+ }
78
+ }
79
+ }
80
+ //# sourceMappingURL=secret-store-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-store-fs.js","sourceRoot":"","sources":["../src/secret-store-fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAoB,MAAM,gBAAgB,CAAC;AAEtF;;;gFAGgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,aAAa;IACP,IAAI,CAAS;IAE9B,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,yCAAyC;IACjF,CAAC;IAED;;;;kGAI8F;IACtF,OAAO,CAAC,GAAW;QACzB,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvE,IAAI,UAAU,CAAC,GAAG,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,4CAA4C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrF,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC;YACvE,MAAM,IAAI,KAAK,CAAC,uDAAuD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChG,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAC,CAAC,gCAAgC;YACtG,MAAM,CAAC,CAAC;QACV,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,qEAAqE;QAC9F,qBAAqB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,8DAA8D;IACjG,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,CAAC,CAAC,CAAC,CAAC;QACZ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,CAAC,CAAC,CAAC,yCAAyC;QACxG,CAAC;IACH,CAAC;CACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/workspace",
3
3
  "description": "Cotal machine-local workstation layer: the ~/.cotal mesh registry, target resolution, preflight, on-disk auth-path I/O, and the command-copy renderer — operator concerns over a local checkout, kept separate from the wire protocol in @cotal-ai/core.",
4
- "version": "0.11.6",
4
+ "version": "0.12.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@cotal-ai/core": "0.11.6"
21
+ "@cotal-ai/core": "0.12.0"
22
22
  },
23
23
  "files": [
24
24
  "dist"
@@ -29,6 +29,6 @@
29
29
  "scripts": {
30
30
  "typecheck": "tsc -p tsconfig.json --noEmit",
31
31
  "build": "tsc -p tsconfig.json",
32
- "test": "tsx smoke/preflight.smoke.ts"
32
+ "test": "tsx smoke/preflight.smoke.ts && tsx smoke/maintenance.smoke.ts"
33
33
  }
34
34
  }