@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.
Files changed (106) hide show
  1. package/dist/bin/mesh.js +11663 -10745
  2. package/dist/bin/mesh.js.map +4 -4
  3. package/dist/build-info.json +2 -2
  4. package/dist/src/commands/agent-api-client.d.ts +17 -0
  5. package/dist/src/commands/agent-api-client.d.ts.map +1 -1
  6. package/dist/src/commands/agent-api-client.js +22 -0
  7. package/dist/src/commands/agent-api-client.js.map +1 -1
  8. package/dist/src/commands/app-check.d.ts +97 -0
  9. package/dist/src/commands/app-check.d.ts.map +1 -0
  10. package/dist/src/commands/app-check.js +502 -0
  11. package/dist/src/commands/app-check.js.map +1 -0
  12. package/dist/src/commands/create-app.d.ts +24 -0
  13. package/dist/src/commands/create-app.d.ts.map +1 -1
  14. package/dist/src/commands/create-app.js +27 -1
  15. package/dist/src/commands/create-app.js.map +1 -1
  16. package/dist/src/commands/dev-doctor.d.ts +28 -12
  17. package/dist/src/commands/dev-doctor.d.ts.map +1 -1
  18. package/dist/src/commands/dev-doctor.js +16 -7
  19. package/dist/src/commands/dev-doctor.js.map +1 -1
  20. package/dist/src/commands/dev.d.ts +22 -0
  21. package/dist/src/commands/dev.d.ts.map +1 -1
  22. package/dist/src/commands/dev.js +124 -9
  23. package/dist/src/commands/dev.js.map +1 -1
  24. package/dist/src/commands/local/docker-runner.d.ts.map +1 -1
  25. package/dist/src/commands/local/docker-runner.js +2 -4
  26. package/dist/src/commands/local/docker-runner.js.map +1 -1
  27. package/dist/src/commands/local/hub-local.d.ts.map +1 -1
  28. package/dist/src/commands/local/hub-local.js +2 -3
  29. package/dist/src/commands/local/hub-local.js.map +1 -1
  30. package/dist/src/commands/local/seed-zitadel.d.ts.map +1 -1
  31. package/dist/src/commands/local/seed-zitadel.js +108 -109
  32. package/dist/src/commands/local/seed-zitadel.js.map +1 -1
  33. package/dist/src/commands/local/stack.d.ts.map +1 -1
  34. package/dist/src/commands/local/stack.js +4 -9
  35. package/dist/src/commands/local/stack.js.map +1 -1
  36. package/dist/src/commands/site.d.ts +3 -0
  37. package/dist/src/commands/site.d.ts.map +1 -0
  38. package/dist/src/commands/site.js +277 -0
  39. package/dist/src/commands/site.js.map +1 -0
  40. package/dist/src/commands/temporal.d.ts +25 -0
  41. package/dist/src/commands/temporal.d.ts.map +1 -1
  42. package/dist/src/commands/temporal.js +2459 -732
  43. package/dist/src/commands/temporal.js.map +7 -1
  44. package/dist/src/commands/vcs/clone.d.ts.map +1 -1
  45. package/dist/src/commands/vcs/clone.js +4 -4
  46. package/dist/src/commands/vcs/clone.js.map +1 -1
  47. package/dist/src/commands/vcs/common.d.ts +34 -0
  48. package/dist/src/commands/vcs/common.d.ts.map +1 -1
  49. package/dist/src/commands/vcs/common.js +52 -0
  50. package/dist/src/commands/vcs/common.js.map +1 -1
  51. package/dist/src/commands/vcs/index.d.ts.map +1 -1
  52. package/dist/src/commands/vcs/index.js +7 -1
  53. package/dist/src/commands/vcs/index.js.map +1 -1
  54. package/dist/src/commands/vcs/propose.d.ts +22 -0
  55. package/dist/src/commands/vcs/propose.d.ts.map +1 -1
  56. package/dist/src/commands/vcs/propose.js +25 -5
  57. package/dist/src/commands/vcs/propose.js.map +1 -1
  58. package/dist/src/commands/vcs/rm.d.ts +18 -0
  59. package/dist/src/commands/vcs/rm.d.ts.map +1 -0
  60. package/dist/src/commands/vcs/rm.js +88 -0
  61. package/dist/src/commands/vcs/rm.js.map +1 -0
  62. package/dist/src/program.d.ts.map +1 -1
  63. package/dist/src/program.js +4 -0
  64. package/dist/src/program.js.map +1 -1
  65. package/dist/src/utils/aws-auth.d.ts +10 -0
  66. package/dist/src/utils/aws-auth.d.ts.map +1 -1
  67. package/dist/src/utils/aws-auth.js +35 -0
  68. package/dist/src/utils/aws-auth.js.map +1 -1
  69. package/dist/src/utils/cache-home.d.ts +25 -0
  70. package/dist/src/utils/cache-home.d.ts.map +1 -0
  71. package/dist/src/utils/cache-home.js +36 -0
  72. package/dist/src/utils/cache-home.js.map +1 -0
  73. package/dist/src/utils/temporal-codec.js +207 -294
  74. package/dist/src/utils/temporal-codec.js.map +7 -1
  75. package/fragments/base/index.ts.hbs +1 -1
  76. package/fragments/service/api/package.json.hbs +1 -0
  77. package/fragments/service/api/src/index.ts.hbs +6 -1
  78. package/fragments/temporal/worker/package.json.hbs +1 -0
  79. package/fragments/temporal/worker/src/activities.ts.hbs +6 -5
  80. package/fragments/temporal/worker/src/worker.ts.hbs +7 -1
  81. package/package.json +12 -3
  82. package/skills/core/SKILL.md +15 -3
  83. package/templates/api-auth/AGENTS.md.hbs +6 -0
  84. package/templates/api-auth/README.md.hbs +17 -8
  85. package/templates/api-auth/api/package.json.hbs +2 -1
  86. package/templates/api-auth/api/{src → scripts}/seed-authz.ts.hbs +3 -3
  87. package/templates/api-auth/api/src/index.ts.hbs +20 -9
  88. package/templates/api-auth/index.ts.hbs +79 -25
  89. package/templates/api-auth/package.json.hbs +2 -0
  90. package/templates/api-role-gating/api/package.json.hbs +1 -0
  91. package/templates/api-role-gating/api/src/main.ts.hbs +6 -1
  92. package/templates/api-role-gating/index.ts.hbs +1 -1
  93. package/templates/api-role-gating/package.json.hbs +1 -0
  94. package/templates/external-service/api/package.json.hbs +2 -0
  95. package/templates/external-service/api/src/index.ts.hbs +7 -2
  96. package/templates/external-service/index.ts.hbs +1 -1
  97. package/templates/external-service/package.json.hbs +1 -0
  98. package/templates/workflow/api/package.json.hbs +1 -0
  99. package/templates/workflow/api/src/index.ts.hbs +10 -5
  100. package/templates/workflow/index.ts.hbs +1 -1
  101. package/templates/workflow/package.json.hbs +1 -0
  102. package/templates/workflow/worker/package.json.hbs +1 -0
  103. package/templates/workflow/worker/src/activities.ts.hbs +7 -3
  104. package/templates/workflow/worker/src/encryption-codec.ts +3 -2
  105. package/templates/workflow/worker/src/worker.ts.hbs +7 -1
  106. 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
- console.info(`[API] {{titleCase name}} listening on port ${info.port}`);
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
- // `console.log` is unstructured: the platform ships worker output to Loki, where
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
- console.error("[Worker] Fatal error:", err);
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.15.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": "1.53.0",
37
- "@mesh-tech/workflow-viz": "0.2.1",
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",
@@ -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`**. Installed standalone or
24
- transitively via the app-kit bundle.
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 bundles this CLI; consumers usually invoke it as `pnpm mesh <cmd>`
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 (`Service` with platform auth + `/health` open)
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/src/seed-authz.ts` — local schema write + grant
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 src/seed-authz.ts"
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 "./schema.js";
12
- import { spiceDbHttpProvider } from "./spicedb-http-provider.js";
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, SPICEDB_ENDPOINT, SPICEDB_PRESHARED_KEY
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`); in the cloud this is the app's own
33
- // audience and consumers request its project-audience scope.
34
- const AUTH_AUDIENCE = process.env.AUTH_AUDIENCE ?? "{{name}}";
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({ issuer: ZITADEL_ISSUER, audience: AUTH_AUDIENCE }),
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 the platform injects SPICEDB_PRESHARED_KEY; fail
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("SPICEDB_PRESHARED_KEY is required in production");
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
- console.log(`{{name}} api listening on :${PORT} (issuer: ${ZITADEL_ISSUER})`);
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
- * Infrastructure: AppEnvironment + API Service behind platform auth
5
- * (oauth2-proxy sidecar against the platform Zitadel).
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
- * Local: mesh start && mesh dev (runtime auth only — no proxy sidecar)
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 * as path from "node:path";
13
- import { fileURLToPath } from "node:url";
14
- import { mesh } from "@mesh-tech/app-kit";
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
- // --- Environment ---
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: "{{name}}",
62
+ appName,
27
63
  namespace: true,
28
- rootDir: __dirname,
64
+ database: false,
65
+ zitadelAppProjectId: identity.projectId,
29
66
  });
30
67
 
31
- // --- Services ---
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
- // The oauth2-proxy sidecar needs its own OIDC client in the platform
34
- // Zitadel. Approved tenant-auth pattern: one Zitadel PROJECT per app (in
35
- // your app tenant's org), one APPLICATION per service create the app's
36
- // project if it doesn't exist, add a web application for this service, and
37
- // set both values on the stack:
38
- // pulumi config set {{name}}:authClientId <clientId>
39
- // pulumi config set --secret {{name}}:authClientSecret <clientSecret>
40
- const appConfig = new pulumi.Config("{{name}}");
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
- auth: {
48
- provider: "mesh", // issuer auto-derived from the platform Zitadel
49
- clientId: appConfig.require("authClientId"),
50
- clientSecret: appConfig.requireSecret("authClientSecret"),
51
- allowUnauthenticated: ["/health"],
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;
@@ -11,6 +11,8 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@mesh-tech/app-kit": "latest",
14
+ "@mesh-tech/authz": "latest",
15
+ "@mesh-tech/infra-components": "latest",
14
16
  "@pulumi/pulumi": "^3.214.1"
15
17
  },
16
18
  "devDependencies": {
@@ -16,6 +16,7 @@
16
16
  "dependencies": {
17
17
  "@mesh-tech/authn": "latest",
18
18
  "@mesh-tech/authz": "latest",
19
+ "@mesh-tech/logger": "latest",
19
20
  "hono": "^4.0.0",
20
21
  "@hono/node-server": "^1.0.0"
21
22
  },
@@ -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
- console.log(`{{name}} api listening on :${port} (role-gating)`);
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,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@mesh-tech/app-kit": "latest",
14
+ "@mesh-tech/infra-components": "latest",
14
15
  "@mesh-tech/authz": "latest",
15
16
  "@pulumi/pulumi": "^3.214.1"
16
17
  },
@@ -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
- console.log(`[vendorpay] quote requested amount=${amount} ${response.status}`);
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
- console.log(`[API] {{titleCase name}} API listening on port ${port}`);
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);