@stackbone/cli 0.3.5 → 0.4.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,75 @@
1
+ -- 0071_api_keys.sql — the per-workspace API KEYS a caller outside the browser
2
+ -- presents to reach an agent or a workflow on this box (PLA-1124).
3
+ --
4
+ -- Numbered 0056 against `next`, whose last migration is 0055. The runner keys
5
+ -- `schema_migrations` on the four-digit prefix alone, so two branches that both
6
+ -- take the next free number half-apply and then report a checksum mismatch
7
+ -- pointing at the wrong problem. `scripts/check-migration-numbers.mjs` is the
8
+ -- pre-merge gate; renumber against the tip you merge into.
9
+ --
10
+ -- ONE migration, and it is the whole story. Unlike `connector`/`connection_grant`
11
+ -- these keys have no control-plane twin to stay in step with: a key is minted by
12
+ -- the box, verified by the box and revoked on the box, so there is nothing to
13
+ -- dual-home and no idempotent-DDL-in-TS copy to keep beside this file. Every
14
+ -- topology — a cloud-hosted box, a self-host install and the `stackbone dev`
15
+ -- emulator — runs its own Postgres and applies this same file at boot
16
+ -- (`apply-migrations.ts`, reached from `workspace-runtime`'s
17
+ -- `apply-platform-schema.ts`), so there is one place the table comes from.
18
+ --
19
+ -- WHAT IS STORED, AND WHAT DELIBERATELY IS NOT. A key is
20
+ -- `sbk_live_<selector>_<secret>`. The SELECTOR names the row and is public — it
21
+ -- is what a lookup matches and what a masked read shows. The SECRET is 256 bits
22
+ -- of randomness and is never written anywhere: `secret_digest` holds its SHA-256
23
+ -- in lowercase hex, and the plaintext exists only inside the create response.
24
+ -- There is deliberately NO column a plaintext key could be put in, which is what
25
+ -- `libs/shared/box-schema/src/lib/schema/api-keys.spec.ts` pins.
26
+ --
27
+ -- NO FOREIGN KEYS, for the same reason the activity trail 0055 creates has none:
28
+ -- `organization_id`, `created_by` and `revoked_by` are the control plane's ids
29
+ -- and neither an `organization` nor a `user` table exists inside a box.
30
+ -- `organization_id` stays NOT NULL — a box nobody has registered records the nil
31
+ -- uuid, which says "this box answers for no organization yet" and never claims a
32
+ -- tenant it does not have. The two actor columns are nullable because a
33
+ -- `stackbone dev` mint happens with nobody signed in, and the honest row for it
34
+ -- names no actor instead of borrowing somebody's name.
35
+ --
36
+ -- REVOCATION IS A LIFECYCLE TRANSITION, NEVER A DELETE. `revoked_at` and
37
+ -- `revoked_by` are set once and never cleared, and no code path removes a row.
38
+ -- A key that stopped working is exactly the thing an operator reconstructs
39
+ -- months later — "who turned this off, and when" is unanswerable from an absent
40
+ -- row. `expires_at` is nullable on purpose: NULL means "valid until revoked",
41
+ -- which is a choice an operator made and not a missing value.
42
+ CREATE TABLE IF NOT EXISTS stackbone_platform.api_keys (
43
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
44
+ organization_id uuid NOT NULL,
45
+ name text NOT NULL,
46
+ selector text NOT NULL,
47
+ secret_digest text NOT NULL,
48
+ permissions jsonb NOT NULL DEFAULT '[]'::jsonb,
49
+ expires_at timestamptz,
50
+ last_used_at timestamptz,
51
+ revoked_at timestamptz,
52
+ revoked_by uuid,
53
+ created_by uuid,
54
+ created_at timestamptz NOT NULL DEFAULT now()
55
+ );
56
+
57
+ -- The lookup the invocation wires will match a presented key on, and the reason
58
+ -- the selector exists at all: one index probe, on a value that is not a secret,
59
+ -- before any digest is compared. UNIQUE because two rows sharing a selector
60
+ -- would make that probe ambiguous — and the mint draws 64 bits, so a collision
61
+ -- is a bug worth failing the insert over rather than resolving arbitrarily.
62
+ CREATE UNIQUE INDEX IF NOT EXISTS api_keys_selector_idx
63
+ ON stackbone_platform.api_keys (selector);
64
+ --> statement-breakpoint
65
+ -- "Every key this workspace holds", newest first — the management list's order.
66
+ CREATE INDEX IF NOT EXISTS api_keys_organization_idx
67
+ ON stackbone_platform.api_keys (organization_id, created_at);
68
+ --> statement-breakpoint
69
+
70
+ -- A digest is not a credential, but it IS the input to an offline guess, and the
71
+ -- read-only SQL Explorer role has no use for one. Kept out of its reach exactly
72
+ -- like `secrets` (0011) and `browser_auth_profiles` (0024): the blanket
73
+ -- `ALTER DEFAULT PRIVILEGES … GRANT SELECT` in 0005 fires on every table created
74
+ -- afterwards, so a table-level REVOKE is what actually strips it.
75
+ REVOKE ALL ON stackbone_platform.api_keys FROM stackbone_viewer;
@@ -0,0 +1,23 @@
1
+ -- 0061_control_plane_identity_mcp_resource.sql — the identifier this box's MCP
2
+ -- resource is registered under, and the exact `aud` of every MCP access token
3
+ -- minted for it (PLA-872).
4
+ --
5
+ -- It belongs on the row 0046 created for the same reason the pair there does: a
6
+ -- box answers for exactly one installation and serves exactly one resource, and
7
+ -- both are learned in the same teach call. Split across two tables, a re-teach
8
+ -- that moved the box to a new hostname could land one and not the other, and the
9
+ -- box would go on verifying tokens minted for an address nobody reaches.
10
+ --
11
+ -- NULLABLE, and that is the whole back-compat story. A box taught by a wizard or
12
+ -- a `stackbone link` from before this column has the pair and no resource: it is
13
+ -- not a broken row, it is a box that cannot serve OAuth MCP yet and answers
14
+ -- `503 mcp_not_ready` at `/mcp` until somebody registers it again. Defaulting it
15
+ -- to anything would be inventing the box's own external URL, which is the one
16
+ -- thing a container cannot know about itself.
17
+ --
18
+ -- `text`, not a domain or a check: what makes a value canonical is
19
+ -- `canonicalMcpResource` in `@stackbone/validators`, the box re-runs it on the
20
+ -- teach call before writing, and a CHECK constraint here could only ever be a
21
+ -- second, weaker copy of that rule.
22
+ ALTER TABLE stackbone_platform.runtime_control_plane_identity
23
+ ADD COLUMN IF NOT EXISTS mcp_resource text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "@clack/prompts": "^1.2.0",
20
20
  "@hono/node-server": "^2.0.0",
21
21
  "@hono/zod-openapi": "^1.6.1",
22
+ "@modelcontextprotocol/server": "2.0.0",
22
23
  "@scalar/hono-api-reference": "^0.11.16",
23
24
  "ajv": "^8.20.0",
24
25
  "ajv-formats": "^3.0.1",
@@ -28,6 +29,7 @@
28
29
  "drizzle-kit": "^0.31.10",
29
30
  "drizzle-orm": "^0.45.2",
30
31
  "hono": "^4.12.15",
32
+ "ioredis": "^5.10.1",
31
33
  "jose": "^6.2.3",
32
34
  "libsodium-wrappers": "^0.8.4",
33
35
  "open": "^11.0.0",
Binary file
Binary file