@mesh-tech/mesh-cli 0.15.0 → 0.17.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/dist/bin/mesh.js +11663 -10745
- package/dist/bin/mesh.js.map +4 -4
- package/dist/build-info.json +2 -2
- package/dist/src/commands/agent-api-client.d.ts +17 -0
- package/dist/src/commands/agent-api-client.d.ts.map +1 -1
- package/dist/src/commands/agent-api-client.js +22 -0
- package/dist/src/commands/agent-api-client.js.map +1 -1
- package/dist/src/commands/app-check.d.ts +97 -0
- package/dist/src/commands/app-check.d.ts.map +1 -0
- package/dist/src/commands/app-check.js +502 -0
- package/dist/src/commands/app-check.js.map +1 -0
- package/dist/src/commands/create-app.d.ts +24 -0
- package/dist/src/commands/create-app.d.ts.map +1 -1
- package/dist/src/commands/create-app.js +27 -1
- package/dist/src/commands/create-app.js.map +1 -1
- package/dist/src/commands/dev-doctor.d.ts +28 -12
- package/dist/src/commands/dev-doctor.d.ts.map +1 -1
- package/dist/src/commands/dev-doctor.js +16 -7
- package/dist/src/commands/dev-doctor.js.map +1 -1
- package/dist/src/commands/dev.d.ts +22 -0
- package/dist/src/commands/dev.d.ts.map +1 -1
- package/dist/src/commands/dev.js +124 -9
- package/dist/src/commands/dev.js.map +1 -1
- package/dist/src/commands/local/docker-runner.d.ts.map +1 -1
- package/dist/src/commands/local/docker-runner.js +2 -4
- package/dist/src/commands/local/docker-runner.js.map +1 -1
- package/dist/src/commands/local/hub-local.d.ts.map +1 -1
- package/dist/src/commands/local/hub-local.js +2 -3
- package/dist/src/commands/local/hub-local.js.map +1 -1
- package/dist/src/commands/local/seed-zitadel.d.ts.map +1 -1
- package/dist/src/commands/local/seed-zitadel.js +108 -109
- package/dist/src/commands/local/seed-zitadel.js.map +1 -1
- package/dist/src/commands/local/stack.d.ts.map +1 -1
- package/dist/src/commands/local/stack.js +4 -9
- package/dist/src/commands/local/stack.js.map +1 -1
- package/dist/src/commands/site.d.ts +3 -0
- package/dist/src/commands/site.d.ts.map +1 -0
- package/dist/src/commands/site.js +277 -0
- package/dist/src/commands/site.js.map +1 -0
- package/dist/src/commands/temporal.d.ts +25 -0
- package/dist/src/commands/temporal.d.ts.map +1 -1
- package/dist/src/commands/temporal.js +2459 -732
- package/dist/src/commands/temporal.js.map +7 -1
- package/dist/src/commands/vcs/clone.d.ts.map +1 -1
- package/dist/src/commands/vcs/clone.js +4 -4
- package/dist/src/commands/vcs/clone.js.map +1 -1
- package/dist/src/commands/vcs/common.d.ts +34 -0
- package/dist/src/commands/vcs/common.d.ts.map +1 -1
- package/dist/src/commands/vcs/common.js +52 -0
- package/dist/src/commands/vcs/common.js.map +1 -1
- package/dist/src/commands/vcs/index.d.ts.map +1 -1
- package/dist/src/commands/vcs/index.js +7 -1
- package/dist/src/commands/vcs/index.js.map +1 -1
- package/dist/src/commands/vcs/propose.d.ts +22 -0
- package/dist/src/commands/vcs/propose.d.ts.map +1 -1
- package/dist/src/commands/vcs/propose.js +25 -5
- package/dist/src/commands/vcs/propose.js.map +1 -1
- package/dist/src/commands/vcs/rm.d.ts +18 -0
- package/dist/src/commands/vcs/rm.d.ts.map +1 -0
- package/dist/src/commands/vcs/rm.js +88 -0
- package/dist/src/commands/vcs/rm.js.map +1 -0
- package/dist/src/program.d.ts.map +1 -1
- package/dist/src/program.js +4 -0
- package/dist/src/program.js.map +1 -1
- package/dist/src/utils/aws-auth.d.ts +10 -0
- package/dist/src/utils/aws-auth.d.ts.map +1 -1
- package/dist/src/utils/aws-auth.js +35 -0
- package/dist/src/utils/aws-auth.js.map +1 -1
- package/dist/src/utils/cache-home.d.ts +25 -0
- package/dist/src/utils/cache-home.d.ts.map +1 -0
- package/dist/src/utils/cache-home.js +36 -0
- package/dist/src/utils/cache-home.js.map +1 -0
- package/dist/src/utils/temporal-codec.js +207 -294
- package/dist/src/utils/temporal-codec.js.map +7 -1
- package/fragments/base/index.ts.hbs +1 -1
- package/fragments/service/api/package.json.hbs +1 -0
- package/fragments/service/api/src/index.ts.hbs +6 -1
- package/fragments/temporal/worker/package.json.hbs +1 -0
- package/fragments/temporal/worker/src/activities.ts.hbs +6 -5
- package/fragments/temporal/worker/src/worker.ts.hbs +7 -1
- package/package.json +12 -3
- package/skills/core/SKILL.md +15 -3
- package/templates/api-auth/AGENTS.md.hbs +6 -0
- package/templates/api-auth/README.md.hbs +17 -8
- package/templates/api-auth/api/package.json.hbs +2 -1
- package/templates/api-auth/api/{src → scripts}/seed-authz.ts.hbs +3 -3
- package/templates/api-auth/api/src/index.ts.hbs +20 -9
- package/templates/api-auth/index.ts.hbs +79 -25
- package/templates/api-auth/package.json.hbs +2 -0
- package/templates/api-role-gating/api/package.json.hbs +1 -0
- package/templates/api-role-gating/api/src/main.ts.hbs +6 -1
- package/templates/api-role-gating/index.ts.hbs +1 -1
- package/templates/api-role-gating/package.json.hbs +1 -0
- package/templates/external-service/api/package.json.hbs +2 -0
- package/templates/external-service/api/src/index.ts.hbs +7 -2
- package/templates/external-service/index.ts.hbs +1 -1
- package/templates/external-service/package.json.hbs +1 -0
- package/templates/workflow/api/package.json.hbs +1 -0
- package/templates/workflow/api/src/index.ts.hbs +10 -5
- package/templates/workflow/index.ts.hbs +1 -1
- package/templates/workflow/package.json.hbs +1 -0
- package/templates/workflow/worker/package.json.hbs +1 -0
- package/templates/workflow/worker/src/activities.ts.hbs +7 -3
- package/templates/workflow/worker/src/encryption-codec.ts +3 -2
- package/templates/workflow/worker/src/worker.ts.hbs +7 -1
- package/templates/workflow/worker/src/workflows.ts.hbs +3 -3
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
import { Hono } from "hono";
|
|
18
18
|
import { serve } from "@hono/node-server";
|
|
19
|
+
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
19
20
|
{{#if temporal}}
|
|
20
21
|
import { createTemporalClient } from "@mesh-tech/app-kit/temporal-client";
|
|
21
22
|
{{/if}}
|
|
22
23
|
|
|
23
24
|
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
25
|
+
// Every service logs through @mesh-tech/logger (the Mesh app contract, gate
|
|
26
|
+
// 0.4): structured, masked, trace-correlated lines the Hub can search.
|
|
27
|
+
const log = createLogger({ service: "{{name}}-api" });
|
|
24
28
|
const app = new Hono();
|
|
29
|
+
app.use("*", requestLogger(log));
|
|
25
30
|
|
|
26
31
|
{{#if temporal}}
|
|
27
32
|
const { withClient } = await createTemporalClient();
|
|
@@ -51,5 +56,5 @@ app.post("/workflow", async (c) => {
|
|
|
51
56
|
|
|
52
57
|
// Start server
|
|
53
58
|
serve({ fetch: app.fetch, port: PORT, hostname: "0.0.0.0" }, (info) => {
|
|
54
|
-
|
|
59
|
+
log.info({ port: info.port }, "{{titleCase name}} API listening");
|
|
55
60
|
});
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@mesh-tech/app-kit": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
|
|
14
|
+
"@mesh-tech/logger": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
|
|
14
15
|
"@temporalio/activity": "^1.11.7",
|
|
15
16
|
"@temporalio/client": "^1.11.7",
|
|
16
17
|
"@temporalio/common": "^1.11.7",
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Activities handle non-deterministic operations:
|
|
5
5
|
* API calls, database queries, external services, etc.
|
|
6
|
+
*
|
|
7
|
+
* Log with `log` from @temporalio/activity: it carries the activity context and
|
|
8
|
+
* reaches the platform logger the worker installs — never the console.
|
|
6
9
|
*/
|
|
7
10
|
|
|
11
|
+
import { log } from "@temporalio/activity";
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Generate a greeting message
|
|
10
15
|
*/
|
|
11
16
|
export async function greet(name: string): Promise<string> {
|
|
12
|
-
|
|
13
|
-
// a bare string cannot be filtered by service, activity, or correlation id.
|
|
14
|
-
// `console.info` is the lint-permitted level; a real activity should use
|
|
15
|
-
// `@mesh-tech/logger` so its fields are queryable.
|
|
16
|
-
console.info(`[Activity] Generating greeting for ${name}`);
|
|
17
|
+
log.info("Generating greeting", { name });
|
|
17
18
|
return `Hello, ${name}!`;
|
|
18
19
|
}
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createTemporalWorker, resolveWorkflows } from "@mesh-tech/app-kit/temporal";
|
|
9
|
+
import { createLogger } from "@mesh-tech/logger";
|
|
9
10
|
import * as activities from "./activities.js";
|
|
10
11
|
|
|
12
|
+
// The worker process logs through @mesh-tech/logger (the Mesh app contract,
|
|
13
|
+
// gate 0.4). Activities use `log` from @temporalio/activity and workflows `log`
|
|
14
|
+
// from @temporalio/workflow — both route to the Runtime logger app-kit installs.
|
|
15
|
+
const log = createLogger({ service: "{{name}}-worker" });
|
|
16
|
+
|
|
11
17
|
async function run() {
|
|
12
18
|
const { worker, cleanup } = await createTemporalWorker({
|
|
13
19
|
activities,
|
|
@@ -22,6 +28,6 @@ async function run() {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
run().catch((err) => {
|
|
25
|
-
|
|
31
|
+
log.error({ err }, "Fatal error");
|
|
26
32
|
process.exit(1);
|
|
27
33
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mesh-tech/mesh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "CLI for Mesh platform development utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -30,11 +30,19 @@
|
|
|
30
30
|
"types": "./dist/src/index.d.ts",
|
|
31
31
|
"default": "./dist/src/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./temporal": {
|
|
34
|
+
"types": "./dist/src/commands/temporal.d.ts",
|
|
35
|
+
"default": "./dist/src/commands/temporal.js"
|
|
36
|
+
},
|
|
37
|
+
"./temporal-codec": {
|
|
38
|
+
"types": "./dist/src/utils/temporal-codec.d.ts",
|
|
39
|
+
"default": "./dist/src/utils/temporal-codec.js"
|
|
40
|
+
},
|
|
33
41
|
"./package.json": "./package.json"
|
|
34
42
|
},
|
|
35
43
|
"devDependencies": {
|
|
36
|
-
"@mesh-tech/tsconfig": "
|
|
37
|
-
"@mesh-tech/workflow-viz": "0.2.
|
|
44
|
+
"@mesh-tech/tsconfig": "2.0.0",
|
|
45
|
+
"@mesh-tech/workflow-viz": "0.2.2",
|
|
38
46
|
"@tanstack/intent": "^0.3.6",
|
|
39
47
|
"@types/node": "^20.0.0",
|
|
40
48
|
"tsx": "^4.20.4"
|
|
@@ -48,6 +56,7 @@
|
|
|
48
56
|
"@inquirer/input": "^4.2.1",
|
|
49
57
|
"@inquirer/password": "^4.0.17",
|
|
50
58
|
"@inquirer/select": "^4.3.1",
|
|
59
|
+
"@mesh-tech/agent-contracts": "0.2.0",
|
|
51
60
|
"@mesh-tech/secrets": "0.1.0",
|
|
52
61
|
"chalk": "^5.3.0",
|
|
53
62
|
"commander": "^12.1.0",
|
package/skills/core/SKILL.md
CHANGED
|
@@ -20,8 +20,9 @@ metadata:
|
|
|
20
20
|
|
|
21
21
|
# `@mesh-tech/mesh-cli`
|
|
22
22
|
|
|
23
|
-
Developer CLI for Mesh Platform. Binary name is **`mesh`**.
|
|
24
|
-
|
|
23
|
+
Developer CLI for Mesh Platform. Binary name is **`mesh`**. Install it explicitly —
|
|
24
|
+
`@mesh-tech/app-kit` ships a `mesh` bin shim onto it but declares it an **optional
|
|
25
|
+
peer**, so it is never installed transitively.
|
|
25
26
|
|
|
26
27
|
## Running it (MESH-2261)
|
|
27
28
|
|
|
@@ -96,6 +97,7 @@ Top-level commands (each `register*` is wired in `src/program.ts`, which `bin/me
|
|
|
96
97
|
| `mesh start` / `mesh stop` / `mesh status` | — | Full-local Mesh platform via docker compose (project `mesh-local`, assets ship in the CLI package): Postgres, Temporal (+UI), Zitadel, SpiceDB, ministack (local AWS fabric: SSM/Secrets/S3 registry), memcached, OpenSearch, a **local mailbox** (Mailpit — SMTP :1025 wired into Zitadel, inbox at http://localhost:8025, so user-activation and password-reset mail is testable locally instead of vanishing), **and the hosted observability backends** (OTel collector + Loki + Tempo + Prometheus — the Hub's logs/traces/metrics views run unchanged). Seeds tenant `local`/env `dev` on first boot and reconciles every Zitadel-provisioned app tenant into the registry. **No AWS creds, VPN, or Pulumi state needed — Docker only.** The **Hub is included by default** — three overlay services, `hub-api` + `hub-ui` + `hub-auth` (an oauth2-proxy; it is what binds the front-door port and proxies to `hub-ui`, so front-door 401s/redirect loops are in ITS logs, not `hub-api`'s) — the front door at http://localhost:9000 — `MESH_HUB_PORT=<port>` publishes it elsewhere when 9000 is taken; export it for `mesh status` too (images build once from the published @mesh-tech/hub tarball; needs `mesh registry login` the first time; `--no-hub` opts out). Auth contract (MESH-2479): `--with-hub` refreshes to the latest published hub after preflighting the CodeArtifact token — with an expired/missing token it falls back to the already-built local images (warn + fixing command), or fails fast naming `mesh registry login --profile mesh-dev` when none exist; the default (no-flag) path with an expired/missing token starts hub-less and names the same fix. Non-TTY runs (CI, agents, piped output) get an `[HH:MM:SS]` prefix on every step line plus a heartbeat during long fetches/builds — `MESH_LOG_TIMESTAMPS=1/0` forces the prefix on/off (default: on when stderr is not a TTY). | `start`: `--no-seed`, `--no-hub`, `--with-hub`; `stop`: `--destroy` (drops volumes/seeds); `status`: `--json` |
|
|
97
98
|
| `mesh hub` | `dev` | One-command local Hub over a running dev-local stack. Launches the CURRENT-CODE Hub (apps/hub api+ui from a mesh-platform checkout) pointed at an existing `mesh dev` session — auto-assembles `HUB_TENANT`/`HUB_SCOPE_ENV`/`HUB_SCOPE_TENANTS` (session platform context), `TEMPORAL_ADDRESS` (session tunnel), `AWS_*` (session scratch profile), `DEV_USER_*`+`DEV_USER_TOKEN_URL` (session dev identity; starts its own token-server if the session predates it, so tokens stay fresh past ~1h). Refuses to launch with an empty tenant scope (which the Hub renders as silently blank). Runs in tmux session `mesh-hub-dev`. | `dev`: `--session <name>`, `--tenants <csv>`, `--port` (or `MESH_HUB_DEV_PORT`, default 9000; the containerized `mesh start` Hub uses the separate `MESH_HUB_PORT`), `--api-port <3002>`, `--platform-dir <dir>` (or `MESH_PLATFORM_DIR`), `--print-env`, `--kill` |
|
|
98
99
|
| `mesh create-app` | — | Scaffold a tenant app. Composable mode (primitives) or legacy template mode. Interactive when TTY + missing flags. Run at the root of an **empty** `{tenant}-mesh-apps` git clone (a `.git` with no `package.json`/`apps/`/`tenants/` and no enclosing pnpm workspace), it first generates the repo-level shape — `pnpm-workspace.yaml` over `apps/*`, private root `package.json`, scoped-registry `.npmrc`, base `tsconfig.json`, `.gitignore`, `README.md`, `apps/` — then scaffolds the app. Never overwrites an existing file. | `--tenant`, `--name`, `--primitives <csv>` (`service,database,temporal,bucket`), `--template <workflow\|api-auth\|api-role-gating\|external-service>`, `--test` (writes to `tests/tenants/`) |
|
|
100
|
+
| `mesh app` | `check [apps...]` | Checks tenant apps against the **Mesh app contract** (`@mesh-tech/app-kit#apps` → `references/app-contract.md`): a UI is its own Service (`UI_IN_API`, `NO_UI_SERVICE`), the app registers with the Hub (`NO_REGISTER`), sign-in goes through the platform proxy (`CUSTOM_SESSION_AUTH`), logs go through `@mesh-tech/logger` (`NON_PLATFORM_LOGGER`), and people/roles/keys are surfaced through the Hub (`NO_AUTHZ_POINTER`, `METADATA_UNPUBLISHED`, `IAC_GRANTS`, `PASSWORD_STORE`, `EMAIL_ALLOWLIST`, `USERS_TABLE`); a tenant-local auth lib is advisory (`LOCAL_AUTH_LIB`). One check per code on the `mesh dev doctor` engine; exit 1 on any BLOCK. The SAME gate `mesh create-app` runs after scaffolding, tenant CI runs beside `mesh skills sync --check`, and the platform reviewer runs on a PR. Default: every `apps/*` with a Pulumi program. | `[apps...]` (repo-relative dirs), `--root <dir>`, `--json`, `--verbose` |
|
|
99
101
|
| `mesh init` | `app-tenant` | The repo doctor (A1–A3): read-only check pipeline — CLI auth, registry access, public npm not hijacked, platform reachable (Hub API), app-tenant registration, deployer role, repo shape, agent skills — each failure names the exact `mesh` fix. **Registry access** is a live probe of the CodeArtifact token, not a grep of `~/.npmrc`: an expired token `fail`s (it used to `pass`), an unreachable registry `warn`s (a network fault is not an auth fault). **Public npm not hijacked** fails when `~/.npmrc` carries an unscoped `registry=<codeartifact>` line, which makes CodeArtifact your default registry — one `mesh registry login` fixes both. Exit 0 iff all pass; re-run any time. `--fix` applies developer-scope fixes (registry token, skills sync); operator actions stay remediations — and `--profile <p>` is the SSO profile `--fix` logs into CodeArtifact with, echoed back in the `fix:` command each check prints so the advertised command is the one that runs. | `app-tenant`: `--tenant <name>`, `--context <ctx>` (default `local`), `--hub-url <url>`, `--fix`, `--profile <sso>`, `--json` |
|
|
100
102
|
| `mesh skills` | `sync` | Agent-skill distribution (D1/D2): installs the base building-with-Mesh skills into the repo's `.claude/skills/mesh-*/` (managed-marker files) and wires TanStack-Intent discovery for the pattern skills shipping inside `@mesh-tech/*` packages (`.intent/hooks/` gate + `.claude/settings.json` SessionStart hook + AGENTS.md fence — the PR #2356 mechanism, vendored). Idempotent; runs automatically after `mesh create-app`. | `sync`: `--check` (CI/doctor: exit 1 when missing/stale), `--root <path>` |
|
|
101
103
|
| `mesh docs` | `build`, `portal`, `cli-reference` | `build` builds/validates an **app's** OpenAPI specs from `docs/docs.config.json` (file copy or SymXchange generation). `start`/`stop`/`list` are the simple front door: `start` serves the docs on loopback — DETACHED in a tmux session named `mesh-docs` by default (prints the bare URL once live; `stop` kills the session; `--foreground` or any non-TTY runs in the foreground) — serving the working tree in a mesh-platform checkout, or the published `@mesh-tech/docs` artifact fetched from the role-gated CodeArtifact registry anywhere else (docs version == the @mesh-tech/* baseline it describes; `-v` pins, default latest; cached under `~/.cache/mesh/docs/`). `list` shows the published docs versions. `portal` assembles the **Mesh developer portal** (docs.meshtech.io) from every `docs.json`-opted-in doc root in the repo — a directory publishes its markdown iff it holds a `docs.json`; route/title/order/nav derive from the tree and each file, so adding a doc is adding one file — then hands the assembled tree to Zudoku (`apps/docs/`): default runs `zudoku build`, `--serve` runs `zudoku dev`, `--assemble-only`/`-o` stop after assembly. Links between published files are rewritten to routes; links to unpublished repo files become blob links. Reserved directory names (`plans/`, `designs/`, `incidents/`, … the frozen list in `src/docs/schema.ts`) are excluded at any depth. `cli-reference` regenerates `docs/portal/generated/cli-reference.md` from THIS command tree, so it can't document a flag the binary lacks. `portal --check` is the CI gate (`pnpm check:docs-portal`): docs.json schema validation, reserved-segment fail-close, broken links, and CLI-reference staleness; `--diff-base <ref>` prints the publish-set diff for PR job summaries. Hidden commands and `--version` are excluded from the reference. | `build`: `-c/--config <path>` (default `docs/docs.config.json`); `portal`: `-o/--out <dir>`, `--assemble-only`, `--serve`, `-p/--port <n>` (default `3000`), `--check`, `--print-manifest`, `--diff-base <ref>`, `--manifest-out <path>`; `cli-reference`: `-o/--out <path>`, `--check`; `start`: `-v/--version <v>` (default: latest), `-p/--port <n>` (default `4400`; `0` is foreground-only), `--dev` (foreground HMR), `--foreground`; `stop`: no flags; `list`: no flags |
|
|
@@ -107,6 +109,8 @@ Top-level commands (each `register*` is wired in `src/program.ts`, which `bin/me
|
|
|
107
109
|
| `mesh conversations` (alias `conv`) | `list`, `show <id>`, `artifacts <id>` | Read a deployed Mesh agent's conversations: list the caller's conversations, render a transcript, or list a conversation's artifacts. Resolves against the same `agent-targets` registry the MCP `agent_*` tools use. | all three: `--target <name>`, `--api-url <url>` (overrides `--target`), `--context <ctx>` (used with `--api-url`, default `mesh.dev`), `--json` (default when stdout isn't a TTY) |
|
|
108
110
|
| `mesh vpn` | `connect <context>`, `disconnect`, `status`, `tunnel <up\|down\|status>`, `api-key`, `pre-auth-key`, `users` | Headscale VPN mgmt via Tailscale + kubectl. **`connect` now defaults to the userspace `tailscaled`** (brew formula, NOT the GUI Tailscale.app) — the same daemon `mesh dev --transport tailscale` / `mesh vpn tunnel` use, so it **runs headless / under the sandbox** and exposes a local SOCKS5 proxy (route a tool via `ALL_PROXY=socks5://127.0.0.1:<port>`). It relays a one-time browser-register URL for Zitadel auth. `--system` opts into the whole-machine GUI Tailscale.app (system TUN) — needs the app installed and **cannot run headless/sandboxed** (Sparkle.framework abort). For auto-forwarded VPC services (Temporal/RDS) prefer **`mesh vpn tunnel up`** or `mesh dev`. `tunnel status` reports runner **ownership** (`Runner: owned (verified)` vs `NOT OWNED (<reason>)` — a port answering a dial is not proof it's mesh's, MESH-2407); `--json` adds `owned` (bool) + `ownership` (reason string). Admin subcommands `exec` into the `headscale-0` pod. | root: `-t/--tenant` (default `mesh`), `-e/--env` (default `dev`), `-n/--namespace`; `connect`/`disconnect`: `--system`; `tunnel up`: `--context <ctx>`; `tunnel down`: `--stop` (stop daemon, keep login), `--logout`; `tunnel status`: `--json`; `pre-auth-key`: `-u/--user` (**required**), `--expiration <24h>`, `--reusable`, `--ephemeral` |
|
|
109
111
|
| `mesh workflow` | `extract-ir <path>` | Parse Temporal workflow TS source → WorkflowIR graph (nodes+edges). Prints JSON or uploads to S3. | `--app <name>`, `--upload <bucket>` (both required together for S3) |
|
|
112
|
+
| `mesh site` | `publish <name> <dir>`, `versions <name>`, `rollback <name> <version>` | Publish a built directory as an OAuth-protected static site served by Studio, content-addressed and incremental: `publish` walks and hashes the tree, uploads only the blobs the site does not already hold, then commits a version and points the site at it; `versions` lists what has been published, newest first; `rollback` points the site back at an earlier version. Three stateless calls against the agent-api, so a run that dies partway leaves nothing to clean up. | all: `--target <name>`, `--api-url <url>`, `--context <ctx>`; `publish`: `--version-id <id>`, `--no-activate` (publish without pointing the site at it) |
|
|
113
|
+
| `mesh vcs` | `clone <repo> [dest]`, `get <repo> <path>`, `propose`, `rm <repo> <paths...>`, `proposals`/`show`/`diff`/`approve`/`reject`/`comment`/`request-changes`, `drafts <list\|show\|create\|submit\|discard>` | Versioned content repos (`mesh.vcs`). Repos are real git repos over smart HTTP but **read-only** — every change goes through a proposal. `clone` wires the bearer auth header into the clone; `propose` submits the working tree (binary content is base64-encoded from the bytes, not the extension, so images survive intact); **`rm` proposes deletions of files or folders with no clone at all**, and `--dry-run` lists what would go without proposing anything. | all: `--url <base>`, `--token <t>`, `--context <ctx>`; `propose`: `-m/--message`, `--revise <id>`, `--merge-parent <sha>`; `rm`: `-m/--message`, `--dry-run` |
|
|
110
114
|
| `mesh temporal` | `describe <wfId> [runId]`, `history <wfId> [runId]`, `recover-conversation <wfId> [runId]`, `capture-history <wfId> [runId]` | Inspect Temporal workflows for the current app/stack; reconstruct an agent conversation transcript from durable history when the worker can't replay it; capture a full history to a local replay fixture. | shared: `--stack`, `--address`, `--namespace`; `history`: `-n/--limit <200>`, `-f/--follow`, `--no-compact`, `-p/--show-payloads` (decrypts via `TEMPORAL_ENCODING_KEY` from the K8s secret); `recover-conversation`: `--out <path>`, `--json`, `--snapshot` (structured `{conversationId,messages,artifacts,focus}` blob); `capture-history`: `--out <path>` (default `~/.mesh/replay-histories/<wfId>.json`, do not commit) | <!-- skill-lint-disable-next-line — row documents the mesh-owned kubectl wrappers; the backticked `aws eks get-token` describes what the CLI runs internally, not an operator step -->
|
|
111
115
|
| `mesh kubectl` / `mesh logs` / `mesh exec` | `kubectl [args…]`, `logs [service\|deployment/x\|k=v]`, `exec <service> -- <cmd>` | Cluster access for a **deployed** app — debug CrashLoops/logs without broader creds. Assumes the app's `mesh:deployerRole`, builds a session kubeconfig from the platform `eks` SSM export (`aws eks get-token` — no `eks:DescribeCluster` / `~/.kube/config` needed), shells out to kubectl. Defaults the namespace to the app's own (deployer RBAC is tenant-namespace-scoped). **Use `--stack <name>` to target the deployed stack (e.g. `dev`, not `dev-local`).** | `--stack <name>`; `logs`: `-f/--follow`, `--tail <200>`, `-c/--container`, `--previous`; `exec`: `-c/--container` |
|
|
112
116
|
|
|
@@ -163,6 +167,7 @@ land in `apps/<name>/`:
|
|
|
163
167
|
mesh create-app --tenant acme --name billing --primitives service,database,temporal
|
|
164
168
|
cd apps/billing && pnpm install
|
|
165
169
|
mesh skills sync # picks up the platform skills now that deps are installed
|
|
170
|
+
mesh app check apps/billing # the Mesh app contract — create-app already ran it; re-run before every PR
|
|
166
171
|
mesh stack init # personal dev-{github-user} stack (deploy:false)
|
|
167
172
|
mesh deploy up --yes # pulumi up via mesh:deployerRole
|
|
168
173
|
mesh dev # tunnels + secrets + tmux (needs VPN for VPC access)
|
|
@@ -303,6 +308,13 @@ MCP `agent_*` tools use; see the dedicated
|
|
|
303
308
|
(registry format, auth model, the `conversation_unavailable` → `mesh temporal
|
|
304
309
|
recover-conversation` break-glass path, and the MCP-vs-CLI relationship).
|
|
305
310
|
|
|
311
|
+
### 7b. Workspace environments are driven from mesh-studio
|
|
312
|
+
|
|
313
|
+
A Studio workspace's environment (live container, grace, build queue) is read and signalled
|
|
314
|
+
by `studio workspace status|restart|reseed|rebuild` from mesh-studio's `@mesh-tech/studio-cli`,
|
|
315
|
+
which builds on this package's `@mesh-tech/mesh-cli/temporal` (`connect`) and
|
|
316
|
+
`@mesh-tech/mesh-cli/temporal-codec` exports. This CLI has no `workspace` command.
|
|
317
|
+
|
|
306
318
|
### 8. Relaunch safety — don't strand live conversations
|
|
307
319
|
|
|
308
320
|
For a **code-only** change, prefer a scoped `mesh dev restart <service>` over a full
|
|
@@ -373,5 +385,5 @@ pulumi, vpn, and log helpers.
|
|
|
373
385
|
|
|
374
386
|
## See also
|
|
375
387
|
|
|
376
|
-
- `@mesh-tech/app-kit#core` — app-kit
|
|
388
|
+
- `@mesh-tech/app-kit#core` — app-kit's `mesh` bin is a shim onto this CLI, which is an **optional peer**: declare `@mesh-tech/mesh-cli` in your repo root's devDependencies. Consumers then invoke it as `pnpm mesh <cmd>`
|
|
377
389
|
- `@mesh-tech/credentials#core` — credential resolution the auth/deploy flows build on
|
|
@@ -14,3 +14,9 @@
|
|
|
14
14
|
provider when @mesh-tech/authz ships it.
|
|
15
15
|
- Pattern skills for authn/authz ship inside those packages (TanStack
|
|
16
16
|
Intent discovers them after `pnpm install`).
|
|
17
|
+
- The app meets the Mesh app contract (`@mesh-tech/app-kit#apps` →
|
|
18
|
+
`references/app-contract.md`): access is surfaced through the Hub by the
|
|
19
|
+
`SpiceDBSchema` + `AppAuthzPointer` wiring in `index.ts`, and every service
|
|
20
|
+
logs through `@mesh-tech/logger`. `mesh app check` verifies it — run it
|
|
21
|
+
before opening a PR; never add a users table, an email allowlist, or a
|
|
22
|
+
manual Zitadel step instead.
|
|
@@ -47,21 +47,30 @@ is per-subject, in SpiceDB, exactly like the deployed platform.
|
|
|
47
47
|
|
|
48
48
|
## Deploy to a tenant environment
|
|
49
49
|
|
|
50
|
-
The deployed Service runs behind the platform's oauth2-proxy sidecar
|
|
51
|
-
(`auth: { provider: "mesh", … }` in `index.ts`), which needs its own OIDC
|
|
52
|
-
client in the platform Zitadel:
|
|
53
|
-
|
|
54
50
|
```bash
|
|
55
51
|
mesh login {{tenant}}.dev
|
|
56
|
-
pulumi config set {{name}}:authClientId <clientId>
|
|
57
|
-
pulumi config set --secret {{name}}:authClientSecret <clientSecret>
|
|
58
52
|
mesh deploy up --yes
|
|
59
53
|
```
|
|
60
54
|
|
|
55
|
+
A browser-facing Service you add later signs people in through the platform
|
|
56
|
+
proxy (`auth: { provider: "mesh" }`). The proxy pins its sign-in callback to
|
|
57
|
+
that Service's own ingress hostname; a Service that answers for more than one
|
|
58
|
+
hostname (`ingress.extraHostnames`) sets `auth.deriveRedirectUrl: true` so each
|
|
59
|
+
host gets its own callback, and registers a redirect URI per host at the IdP.
|
|
60
|
+
|
|
61
|
+
`index.ts` owns the whole access surface, so there is nothing to create by
|
|
62
|
+
hand: `ZitadelAppIdentity` makes the app's Zitadel project + roles,
|
|
63
|
+
`SpiceDBSchema` compiles `api/src/schema.ts` into the app's own SpiceDB and
|
|
64
|
+
publishes the ops-hub metadata, and `AppAuthzPointer` tells the Hub where to
|
|
65
|
+
look. People, roles, API keys and sign-in activity are then administered from
|
|
66
|
+
the Hub's **App → Access** tab — never by a runbook step. The contract behind
|
|
67
|
+
that: `@mesh-tech/app-kit#apps` → `references/app-contract.md`; `mesh app
|
|
68
|
+
check` verifies it.
|
|
69
|
+
|
|
61
70
|
## Layout
|
|
62
71
|
|
|
63
|
-
- `index.ts` — Pulumi program
|
|
72
|
+
- `index.ts` — Pulumi program: Zitadel identity → SpiceDB + schema → `Service` → `AppAuthzPointer`
|
|
64
73
|
- `api/src/index.ts` — Hono app: authn middleware + authz `require()`
|
|
65
74
|
- `api/src/schema.ts` — the authorization schema (single source of truth)
|
|
66
75
|
- `api/src/spicedb-http-provider.ts` — starter `AuthzProvider` (HTTP API)
|
|
67
|
-
- `api/
|
|
76
|
+
- `api/scripts/seed-authz.ts` — local schema write + grant
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
"start": "node dist/index.js",
|
|
9
9
|
"build": "esm-bundle src/index.ts --outfile dist/index.js",
|
|
10
10
|
"check-types": "tsc --noEmit",
|
|
11
|
-
"seed:authz": "tsx
|
|
11
|
+
"seed:authz": "tsx scripts/seed-authz.ts"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@mesh-tech/authn": "latest",
|
|
15
15
|
"@mesh-tech/authz": "latest",
|
|
16
|
+
"@mesh-tech/logger": "latest",
|
|
16
17
|
"@opentelemetry/auto-instrumentations-node": "^0.78.0",
|
|
17
18
|
"@mesh-tech/sinks": "latest",
|
|
18
19
|
"hono": "^4.0.0",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { compileSpiceDBSchema } from "@mesh-tech/authz/compilers";
|
|
11
|
-
import { schema } from "
|
|
12
|
-
import { spiceDbHttpProvider } from "
|
|
11
|
+
import { schema } from "../src/schema.js";
|
|
12
|
+
import { spiceDbHttpProvider } from "../src/spicedb-http-provider.js";
|
|
13
13
|
|
|
14
14
|
const ENDPOINT = process.env.SPICEDB_HTTP_ENDPOINT ?? "http://localhost:8443";
|
|
15
|
-
const KEY = process.env.SPICEDB_PRESHARED_KEY ?? "local-dev-key";
|
|
15
|
+
const KEY = process.env.SPICEDB_TOKEN ?? process.env.SPICEDB_PRESHARED_KEY ?? "local-dev-key";
|
|
16
16
|
|
|
17
17
|
// The Zitadel subject (`sub` claim) of the user to grant. Find it after
|
|
18
18
|
// `mesh login local`: the CLI caches it in ~/.config/mesh/credentials.json,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* GET /api/report permission-gated (needs report:view via SpiceDB → else 403)
|
|
11
11
|
*
|
|
12
12
|
* Environment (mesh dev injects all of these — local or tethered):
|
|
13
|
-
* PORT, ZITADEL_ISSUER,
|
|
13
|
+
* PORT, ZITADEL_ISSUER, ZITADEL_PROJECT_ID, SPICEDB_HTTP_ENDPOINT, SPICEDB_TOKEN
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { Hono } from "hono";
|
|
@@ -20,22 +20,31 @@ import { zitadelJwtScheme } from "@mesh-tech/authn/schemes/zitadel-jwt";
|
|
|
20
20
|
import { createAuthnMiddleware, type AuthnVariables } from "@mesh-tech/authn/hono";
|
|
21
21
|
import { createAuthz } from "@mesh-tech/authz/runtime";
|
|
22
22
|
import { createAuthzMiddleware, type AuthzVariables } from "@mesh-tech/authz/hono";
|
|
23
|
+
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
23
24
|
import { schema } from "./schema.js";
|
|
24
25
|
import { spiceDbHttpProvider } from "./spicedb-http-provider.js";
|
|
25
26
|
|
|
26
27
|
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
27
28
|
const ZITADEL_ISSUER = process.env.ZITADEL_ISSUER ?? "http://localhost:8080";
|
|
29
|
+
// Every service logs through @mesh-tech/logger (the Mesh app contract, gate 0.4).
|
|
30
|
+
const log = createLogger({ service: "{{name}}-api" });
|
|
28
31
|
|
|
29
32
|
// --- authn: who is calling ---
|
|
30
33
|
|
|
31
34
|
// Audience: locally, mesh dev injects the seeded CLI app's clientId
|
|
32
|
-
// (tokens come from `mesh login local`);
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
+
// (tokens come from `mesh login local`); deployed, the app's OWN Zitadel
|
|
36
|
+
// project id (ZitadelAppIdentity in index.ts) is the audience, and passing it
|
|
37
|
+
// as `projectId` makes the scheme read the project-keyed roles claim.
|
|
38
|
+
const ZITADEL_PROJECT_ID = process.env.ZITADEL_PROJECT_ID;
|
|
39
|
+
const AUTH_AUDIENCE = process.env.AUTH_AUDIENCE ?? ZITADEL_PROJECT_ID ?? "{{name}}";
|
|
35
40
|
|
|
36
41
|
const authn = createAuthn({
|
|
37
42
|
schemes: {
|
|
38
|
-
"zitadel-jwt": zitadelJwtScheme({
|
|
43
|
+
"zitadel-jwt": zitadelJwtScheme({
|
|
44
|
+
issuer: ZITADEL_ISSUER,
|
|
45
|
+
audience: AUTH_AUDIENCE,
|
|
46
|
+
...(ZITADEL_PROJECT_ID ? { projectId: ZITADEL_PROJECT_ID } : {}),
|
|
47
|
+
}),
|
|
39
48
|
},
|
|
40
49
|
});
|
|
41
50
|
const { middleware: requireAuth, public: publicRoute } = createAuthnMiddleware(authn);
|
|
@@ -49,13 +58,14 @@ const authz = createAuthz({
|
|
|
49
58
|
provider: spiceDbHttpProvider({
|
|
50
59
|
endpoint: process.env.SPICEDB_HTTP_ENDPOINT ?? "http://localhost:8443",
|
|
51
60
|
// "local-dev-key" is the `mesh start` stack's key — local ONLY. In
|
|
52
|
-
// any deployed env
|
|
53
|
-
// loudly rather than silently authing with the public default.
|
|
61
|
+
// any deployed env `spicedb.link()` (index.ts) injects SPICEDB_TOKEN;
|
|
62
|
+
// fail loudly rather than silently authing with the public default.
|
|
54
63
|
presharedKey:
|
|
64
|
+
process.env.SPICEDB_TOKEN ??
|
|
55
65
|
process.env.SPICEDB_PRESHARED_KEY ??
|
|
56
66
|
(process.env.NODE_ENV === "production"
|
|
57
67
|
? (() => {
|
|
58
|
-
throw new Error("
|
|
68
|
+
throw new Error("SPICEDB_TOKEN is required in production");
|
|
59
69
|
})()
|
|
60
70
|
: "local-dev-key"),
|
|
61
71
|
}),
|
|
@@ -73,6 +83,7 @@ const m = createAuthzMiddleware(authz, {
|
|
|
73
83
|
// --- routes ---
|
|
74
84
|
|
|
75
85
|
const app = new Hono<{ Variables: AuthnVariables & AuthzVariables }>();
|
|
86
|
+
app.use("*", requestLogger(log));
|
|
76
87
|
|
|
77
88
|
// Open: no token needed (also excluded from the oauth2-proxy in the cloud)
|
|
78
89
|
app.get("/health", publicRoute(), (c) => c.json({ status: "ok" }));
|
|
@@ -92,5 +103,5 @@ app.get("/api/report", m.require("report", "view", { id: () => "quarterly" }), (
|
|
|
92
103
|
);
|
|
93
104
|
|
|
94
105
|
serve({ fetch: app.fetch, port: PORT }, () => {
|
|
95
|
-
|
|
106
|
+
log.info({ port: PORT, issuer: ZITADEL_ISSUER }, "{{name}} api listening");
|
|
96
107
|
});
|
|
@@ -1,54 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* {{titleCase name}} — Platform-authenticated API
|
|
2
|
+
* {{titleCase name}} — Platform-authenticated API (authz mode "policy-engine").
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* authn: Zitadel JWTs verified in-process (@mesh-tech/authn) against the app's
|
|
5
|
+
* OWN Zitadel project. authz: schema-driven permission checks in the app's OWN
|
|
6
|
+
* SpiceDB (@mesh-tech/authz). Everything the Hub needs to administer people,
|
|
7
|
+
* roles and API keys for this app is published from here — nothing is created
|
|
8
|
+
* by hand in Zitadel and nothing lives in a runbook (the Mesh app contract,
|
|
9
|
+
* gate H: `@mesh-tech/app-kit#apps` → references/app-contract.md).
|
|
6
10
|
*
|
|
7
|
-
*
|
|
11
|
+
* Wires, IN ORDER:
|
|
12
|
+
* 1. ZitadelConnector — authed provider for the platform Zitadel.
|
|
13
|
+
* 2. ZitadelAppIdentity — this app's OWN project; roles = schema.coarseRoles.
|
|
14
|
+
* 3. AppEnvironment — namespace + providers, bound to the app's project.
|
|
15
|
+
* 4. SpiceDB — the app's OWN SpiceDB instance (shared-DB mode).
|
|
16
|
+
* 5. SpiceDBSchema — compiles api/src/schema.ts to .zed + ops-hub
|
|
17
|
+
* metadata, applies it, exports the metadata to SSM.
|
|
18
|
+
* 6. api Service — Hono app: authn + authz middleware, linked to SpiceDB.
|
|
19
|
+
* 7. AppAuthzPointer — LAST. Tells the Hub where this app's authz lives.
|
|
20
|
+
*
|
|
21
|
+
* Prerequisite: the target platform must have Zitadel enabled — ZitadelConnector
|
|
22
|
+
* eager-reads it at preview time.
|
|
23
|
+
*
|
|
24
|
+
* Local: mesh start && mesh dev (the local platform's Zitadel + SpiceDB)
|
|
8
25
|
* Deploy: mesh deploy up
|
|
9
26
|
*/
|
|
10
27
|
|
|
11
28
|
import * as pulumi from "@pulumi/pulumi";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
|
|
29
|
+
import { mesh } from "@mesh-tech/app-kit/infra";
|
|
30
|
+
import { compileOpsHubMetadata, compileSpiceDBSchema } from "@mesh-tech/authz";
|
|
31
|
+
// The authz schema is the single source of truth, shared by the api (enforces
|
|
32
|
+
// it) and this Pulumi program (compiles it to SpiceDB + ops-hub metadata + roles).
|
|
33
|
+
import { schema as schemaDef } from "./api/src/schema.js";
|
|
15
34
|
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
35
|
const { tenant, platform, stack, deploy } = mesh.apps.getConfig(pulumi);
|
|
36
|
+
const appName = "{{name}}";
|
|
37
|
+
|
|
38
|
+
// --- 1. Connector — authed provider for the platform Zitadel. ---
|
|
39
|
+
const connector = new mesh.auth.ZitadelConnector("idp", { tenant, env: platform.env });
|
|
18
40
|
|
|
19
|
-
// ---
|
|
41
|
+
// --- 2. App identity — this app's OWN Zitadel project + roles. ---
|
|
42
|
+
// The schema's coarse roles become Zitadel PROJECT ROLES: the vocabulary the
|
|
43
|
+
// Hub grants people and API keys against. An `api`-type application is the
|
|
44
|
+
// resource server tokens are minted for (its clientId is the JWT audience).
|
|
45
|
+
const identity = new mesh.auth.ZitadelAppIdentity("identity", {
|
|
46
|
+
connector,
|
|
47
|
+
tenant,
|
|
48
|
+
env: platform.env,
|
|
49
|
+
project: { name: appName },
|
|
50
|
+
roles: [...schemaDef.coarseRoles],
|
|
51
|
+
applications: {
|
|
52
|
+
api: { type: "api" },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
20
55
|
|
|
56
|
+
// --- 3. Environment — bound to the app's own project via zitadelAppProjectId. ---
|
|
21
57
|
const env = new mesh.apps.AppEnvironment("env", {
|
|
22
58
|
tenant,
|
|
23
59
|
platform,
|
|
24
60
|
stack,
|
|
25
61
|
deploy,
|
|
26
|
-
appName
|
|
62
|
+
appName,
|
|
27
63
|
namespace: true,
|
|
28
|
-
|
|
64
|
+
database: false,
|
|
65
|
+
zitadelAppProjectId: identity.projectId,
|
|
29
66
|
});
|
|
30
67
|
|
|
31
|
-
// ---
|
|
68
|
+
// --- 4. SpiceDB — the app's OWN instance (omit `rds` → the platform's shared RDS). ---
|
|
69
|
+
// `httpEnabled` serves the REST gateway the starter spicedb-http-provider talks to.
|
|
70
|
+
const spicedb = new mesh.auth.SpiceDB("spicedb", { env, httpEnabled: true });
|
|
32
71
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
// --- 5. Schema — compile, apply, and publish the ops-hub metadata. ---
|
|
73
|
+
// `opsHubMetadataRef` is the SSM path the pointer below hands to the Hub; without
|
|
74
|
+
// it the Hub's create-key and role catalog answer 409 METADATA_UNPUBLISHED.
|
|
75
|
+
const schema = new mesh.auth.SpiceDBSchema("schema", {
|
|
76
|
+
env,
|
|
77
|
+
spicedb,
|
|
78
|
+
compiled: {
|
|
79
|
+
zed: compileSpiceDBSchema(schemaDef).zed,
|
|
80
|
+
metadata: compileOpsHubMetadata(schemaDef),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
41
83
|
|
|
84
|
+
// --- 6. api Service — in-process authn + authz; SpiceDB reached via link(). ---
|
|
42
85
|
const api = new mesh.apps.Service("api", {
|
|
43
86
|
env,
|
|
87
|
+
title: "{{titleCase name}} API",
|
|
88
|
+
description: "Platform-authenticated API for {{titleCase name}}",
|
|
44
89
|
runtime: "node",
|
|
45
90
|
src: "./api",
|
|
46
91
|
port: 3000,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
// spicedb.link() injects SPICEDB_ENDPOINT (gRPC) + SPICEDB_TOKEN and the IAM
|
|
93
|
+
// to reach the instance; the HTTP gateway address rides alongside for the
|
|
94
|
+
// starter provider.
|
|
95
|
+
link: [spicedb.link()],
|
|
96
|
+
environment: {
|
|
97
|
+
ZITADEL_ISSUER: pulumi.output(env.zitadel).apply((z) => z?.issuer ?? ""),
|
|
98
|
+
ZITADEL_PROJECT_ID: identity.projectId,
|
|
99
|
+
SPICEDB_HTTP_ENDPOINT: spicedb.httpEndpoint,
|
|
52
100
|
},
|
|
53
101
|
ingress: env.buildIngress({
|
|
54
102
|
subdomain: "{{name}}-api",
|
|
@@ -58,8 +106,14 @@ const api = new mesh.apps.Service("api", {
|
|
|
58
106
|
resources: { cpu: "100m", memory: "128Mi" },
|
|
59
107
|
});
|
|
60
108
|
|
|
109
|
+
// --- 7. AppAuthzPointer — LAST. The Hub's App → Access tab reads this. ---
|
|
110
|
+
new mesh.auth.AppAuthzPointer("authz", { env, mode: "policy-engine", opsHubMetadataRef: schema.opsHubMetadataRef });
|
|
111
|
+
|
|
61
112
|
// --- Outputs ---
|
|
62
113
|
|
|
63
114
|
export const app = env.register({
|
|
64
115
|
description: "{{titleCase name}} — platform-authenticated API (authn + authz)",
|
|
65
116
|
});
|
|
117
|
+
|
|
118
|
+
/** The app's own Zitadel project id (grant people and API keys against this). */
|
|
119
|
+
export const zitadelProjectId = identity.projectId;
|
|
@@ -10,17 +10,22 @@ import { serve } from "@hono/node-server";
|
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
import { createAuthnMiddleware, type AuthnVariables } from "@mesh-tech/authn/hono";
|
|
12
12
|
import { createAuthzMiddleware, type AuthzVariables } from "@mesh-tech/authz/hono";
|
|
13
|
+
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
13
14
|
import { createAuthnFromEnv } from "./authn.js";
|
|
14
15
|
import { createAppAuthz } from "./authz.js";
|
|
15
16
|
|
|
16
17
|
type TestAuth = { subjectSub: string; coarseRole?: string | readonly string[] };
|
|
17
18
|
type AppEnv = { Variables: AuthnVariables & AuthzVariables };
|
|
18
19
|
|
|
20
|
+
// Every service logs through @mesh-tech/logger (the Mesh app contract, gate 0.4).
|
|
21
|
+
const log = createLogger({ service: "{{name}}-api" });
|
|
22
|
+
|
|
19
23
|
/** The role-gated route paths (kept in one place so authn + authz agree). */
|
|
20
24
|
const GATED = ["/admin", "/reports"] as const;
|
|
21
25
|
|
|
22
26
|
export const createApp = (deps: { testAuth?: TestAuth } = {}) => {
|
|
23
27
|
const app = new Hono<AppEnv>();
|
|
28
|
+
app.use("*", requestLogger(log));
|
|
24
29
|
const m = createAuthzMiddleware(createAppAuthz());
|
|
25
30
|
|
|
26
31
|
// Open: no token needed.
|
|
@@ -54,5 +59,5 @@ export const createApp = (deps: { testAuth?: TestAuth } = {}) => {
|
|
|
54
59
|
if (process.env.NODE_ENV !== "test" && process.argv[1]?.endsWith("main.js")) {
|
|
55
60
|
const port = Number(process.env.PORT ?? 3000);
|
|
56
61
|
serve({ fetch: createApp().fetch, port });
|
|
57
|
-
|
|
62
|
+
log.info({ port, mode: "role-gating" }, "{{name}} api listening");
|
|
58
63
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Deploy: mesh deploy up
|
|
25
25
|
*/
|
|
26
26
|
import * as pulumi from "@pulumi/pulumi";
|
|
27
|
-
import { mesh } from "@mesh-tech/app-kit";
|
|
27
|
+
import { mesh } from "@mesh-tech/app-kit/infra";
|
|
28
28
|
import { compileOpsHubMetadata } from "@mesh-tech/authz";
|
|
29
29
|
import type { OpsHubMetadata } from "@mesh-tech/authz";
|
|
30
30
|
// The authz schema is the single source of truth, shared by the api (enforces
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@mesh-tech/app-kit": "latest",
|
|
14
|
+
"@mesh-tech/secrets": "latest",
|
|
14
15
|
"@opentelemetry/auto-instrumentations-node": "^0.78.0",
|
|
15
16
|
"@mesh-tech/http-client": "latest",
|
|
17
|
+
"@mesh-tech/logger": "latest",
|
|
16
18
|
"hono": "^4.0.0",
|
|
17
19
|
"@hono/node-server": "^1.0.0"
|
|
18
20
|
},
|
|
@@ -19,6 +19,7 @@ import { Hono } from "hono";
|
|
|
19
19
|
import { resolveCredentials } from "@mesh-tech/app-kit/credentials";
|
|
20
20
|
import { traceExternalCall } from "@mesh-tech/app-kit/tracing";
|
|
21
21
|
import { enhanceFetch } from "@mesh-tech/http-client";
|
|
22
|
+
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
22
23
|
|
|
23
24
|
interface VendorpayCredentials extends Record<string, string> {
|
|
24
25
|
baseUrl: string;
|
|
@@ -27,7 +28,11 @@ interface VendorpayCredentials extends Record<string, string> {
|
|
|
27
28
|
|
|
28
29
|
const vendorFetch = enhanceFetch();
|
|
29
30
|
|
|
31
|
+
// Every service logs through @mesh-tech/logger (the Mesh app contract, gate
|
|
32
|
+
// 0.4): structured, masked, trace-correlated lines the Hub can search.
|
|
33
|
+
const log = createLogger({ service: "{{name}}-api" });
|
|
30
34
|
const app = new Hono();
|
|
35
|
+
app.use("*", requestLogger(log));
|
|
31
36
|
|
|
32
37
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
33
38
|
|
|
@@ -62,7 +67,7 @@ app.post("/payments/quote", async (c) => {
|
|
|
62
67
|
// Log vendor interactions with the service name — these lines are what
|
|
63
68
|
// the Hub's Integrations → Monitoring → Logs view surfaces (it searches
|
|
64
69
|
// consumer logs for mentions of the external service).
|
|
65
|
-
|
|
70
|
+
log.info({ externalService: "vendorpay", amount, status: response.status }, "vendorpay quote requested");
|
|
66
71
|
|
|
67
72
|
if (!response.ok) {
|
|
68
73
|
return c.json(
|
|
@@ -77,5 +82,5 @@ app.post("/payments/quote", async (c) => {
|
|
|
77
82
|
|
|
78
83
|
const port = Number(process.env.PORT ?? 3000);
|
|
79
84
|
serve({ fetch: app.fetch, port }, () => {
|
|
80
|
-
|
|
85
|
+
log.info({ port }, "{{titleCase name}} API listening");
|
|
81
86
|
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as pulumi from "@pulumi/pulumi";
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { mesh } from "@mesh-tech/app-kit";
|
|
19
|
+
import { mesh } from "@mesh-tech/app-kit/infra";
|
|
20
20
|
|
|
21
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
const { tenant, platform, stack, deploy } = mesh.apps.getConfig(pulumi);
|