@mesh-tech/mesh-cli 0.14.0 → 0.16.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 (74) hide show
  1. package/dist/bin/mesh.js +11485 -10591
  2. package/dist/bin/mesh.js.map +4 -4
  3. package/dist/build-info.json +2 -2
  4. package/dist/src/commands/app-check.d.ts +97 -0
  5. package/dist/src/commands/app-check.d.ts.map +1 -0
  6. package/dist/src/commands/app-check.js +502 -0
  7. package/dist/src/commands/app-check.js.map +1 -0
  8. package/dist/src/commands/create-app.d.ts +28 -0
  9. package/dist/src/commands/create-app.d.ts.map +1 -1
  10. package/dist/src/commands/create-app.js +40 -7
  11. package/dist/src/commands/create-app.js.map +1 -1
  12. package/dist/src/commands/dev-doctor.d.ts +28 -12
  13. package/dist/src/commands/dev-doctor.d.ts.map +1 -1
  14. package/dist/src/commands/dev-doctor.js +9 -5
  15. package/dist/src/commands/dev-doctor.js.map +1 -1
  16. package/dist/src/commands/dev.d.ts +11 -0
  17. package/dist/src/commands/dev.d.ts.map +1 -1
  18. package/dist/src/commands/dev.js +70 -1
  19. package/dist/src/commands/dev.js.map +1 -1
  20. package/dist/src/commands/hub/index.d.ts.map +1 -1
  21. package/dist/src/commands/hub/index.js +65 -4
  22. package/dist/src/commands/hub/index.js.map +1 -1
  23. package/dist/src/commands/local/auth-provision.d.ts +34 -0
  24. package/dist/src/commands/local/auth-provision.d.ts.map +1 -1
  25. package/dist/src/commands/local/auth-provision.js +78 -1
  26. package/dist/src/commands/local/auth-provision.js.map +1 -1
  27. package/dist/src/commands/local/hub-local.d.ts +38 -0
  28. package/dist/src/commands/local/hub-local.d.ts.map +1 -1
  29. package/dist/src/commands/local/hub-local.js +137 -0
  30. package/dist/src/commands/local/hub-local.js.map +1 -1
  31. package/dist/src/commands/local/index.d.ts.map +1 -1
  32. package/dist/src/commands/local/index.js +31 -3
  33. package/dist/src/commands/local/index.js.map +1 -1
  34. package/dist/src/commands/local/seed-zitadel.d.ts +10 -3
  35. package/dist/src/commands/local/seed-zitadel.d.ts.map +1 -1
  36. package/dist/src/commands/local/seed-zitadel.js +259 -78
  37. package/dist/src/commands/local/seed-zitadel.js.map +1 -1
  38. package/dist/src/commands/local/stack.d.ts +30 -0
  39. package/dist/src/commands/local/stack.d.ts.map +1 -1
  40. package/dist/src/commands/local/stack.js +48 -0
  41. package/dist/src/commands/local/stack.js.map +1 -1
  42. package/dist/src/program.d.ts.map +1 -1
  43. package/dist/src/program.js +2 -0
  44. package/dist/src/program.js.map +1 -1
  45. package/fragments/base/index.ts.hbs +9 -0
  46. package/fragments/base/package.json.hbs +1 -1
  47. package/fragments/service/api/package.json.hbs +5 -0
  48. package/fragments/service/api/src/index.ts.hbs +6 -4
  49. package/fragments/temporal/worker/package.json.hbs +2 -0
  50. package/fragments/temporal/worker/src/activities.ts.hbs +6 -1
  51. package/fragments/temporal/worker/src/worker.ts.hbs +7 -1
  52. package/fragments/temporal/worker/src/workflows.ts.hbs +4 -1
  53. package/package.json +2 -2
  54. package/skills/core/SKILL.md +3 -1
  55. package/stack/docker-compose.hub.yml +5 -0
  56. package/stack/docker-compose.yml +35 -0
  57. package/templates/api-auth/AGENTS.md.hbs +6 -0
  58. package/templates/api-auth/README.md.hbs +11 -8
  59. package/templates/api-auth/api/package.json.hbs +2 -1
  60. package/templates/api-auth/api/{src → scripts}/seed-authz.ts.hbs +3 -3
  61. package/templates/api-auth/api/src/index.ts.hbs +20 -9
  62. package/templates/api-auth/index.ts.hbs +78 -24
  63. package/templates/api-auth/package.json.hbs +1 -0
  64. package/templates/api-role-gating/api/package.json.hbs +1 -0
  65. package/templates/api-role-gating/api/src/main.ts.hbs +6 -1
  66. package/templates/external-service/api/package.json.hbs +1 -0
  67. package/templates/external-service/api/src/index.ts.hbs +7 -2
  68. package/templates/workflow/api/package.json.hbs +1 -0
  69. package/templates/workflow/api/src/index.ts.hbs +10 -5
  70. package/templates/workflow/worker/package.json.hbs +1 -0
  71. package/templates/workflow/worker/src/activities.ts.hbs +7 -3
  72. package/templates/workflow/worker/src/encryption-codec.ts +3 -2
  73. package/templates/workflow/worker/src/worker.ts.hbs +7 -1
  74. package/templates/workflow/worker/src/workflows.ts.hbs +3 -3
@@ -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
29
  import { mesh } from "@mesh-tech/app-kit";
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,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@mesh-tech/app-kit": "latest",
14
+ "@mesh-tech/authz": "latest",
14
15
  "@pulumi/pulumi": "^3.214.1"
15
16
  },
16
17
  "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
  }
@@ -13,6 +13,7 @@
13
13
  "@mesh-tech/app-kit": "latest",
14
14
  "@opentelemetry/auto-instrumentations-node": "^0.78.0",
15
15
  "@mesh-tech/http-client": "latest",
16
+ "@mesh-tech/logger": "latest",
16
17
  "hono": "^4.0.0",
17
18
  "@hono/node-server": "^1.0.0"
18
19
  },
@@ -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
  });
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@mesh-tech/app-kit": "latest",
14
+ "@mesh-tech/logger": "latest",
14
15
  "@opentelemetry/auto-instrumentations-node": "^0.78.0",
15
16
  "@temporalio/client": "^1.11.7",
16
17
  "hono": "^4.0.0",
@@ -12,11 +12,16 @@
12
12
  import { Hono } from "hono";
13
13
  import { serve } from "@hono/node-server";
14
14
  import { createTemporalClient, type WithClient } from "@mesh-tech/app-kit/temporal-client";
15
+ import { createLogger, requestLogger } from "@mesh-tech/logger";
15
16
 
16
17
  const PORT = parseInt(process.env.PORT ?? "3000", 10);
17
18
  const TEMPORAL_TASK_QUEUE = process.env.TEMPORAL_TASK_QUEUE ?? "{{name}}";
18
19
 
20
+ // Every service logs through @mesh-tech/logger (the Mesh app contract, gate
21
+ // 0.4): structured, masked, trace-correlated lines the Hub can search.
22
+ const log = createLogger({ service: "{{name}}-api" });
19
23
  const app = new Hono();
24
+ app.use("*", requestLogger(log));
20
25
 
21
26
  // createTemporalClient returns a withClient wrapper (auth is applied
22
27
  // per-call); with no TEMPORAL_AUTH_* env it runs unauthenticated — the
@@ -61,7 +66,7 @@ app.post("/workflows/greet", async (c) => {
61
66
  });
62
67
  return c.json({ workflowId, result });
63
68
  } catch (error) {
64
- console.error("[API] Error starting workflow:", error);
69
+ log.error({ err: error }, "Error starting workflow");
65
70
  return c.json({ error: "Failed to start workflow" }, 500);
66
71
  }
67
72
  });
@@ -92,7 +97,7 @@ app.post("/workflows/task", async (c) => {
92
97
  message: "Workflow started. Check Temporal Web UI for progress.",
93
98
  });
94
99
  } catch (error) {
95
- console.error("[API] Error starting workflow:", error);
100
+ log.error({ err: error }, "Error starting workflow");
96
101
  return c.json({ error: "Failed to start workflow" }, 500);
97
102
  }
98
103
  });
@@ -112,7 +117,7 @@ app.get("/workflows/:id", async (c) => {
112
117
  closeTime: describe.closeTime,
113
118
  });
114
119
  } catch (error) {
115
- console.error("[API] Error getting workflow:", error);
120
+ log.error({ err: error }, "Error getting workflow");
116
121
  return c.json({ error: "Failed to get workflow status" }, 500);
117
122
  }
118
123
  });
@@ -121,10 +126,10 @@ app.get("/workflows/:id", async (c) => {
121
126
  initClient()
122
127
  .then(() => {
123
128
  serve({ fetch: app.fetch, port: PORT, hostname: "0.0.0.0" }, () => {
124
- console.log(`[API] {{titleCase name}} API listening on port ${PORT}`);
129
+ log.info({ port: PORT }, "{{titleCase name}} API listening");
125
130
  });
126
131
  })
127
132
  .catch((err) => {
128
- console.error("[API] Failed to initialize:", err);
133
+ log.error({ err }, "Failed to initialize");
129
134
  process.exit(1);
130
135
  });
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@mesh-tech/app-kit": "latest",
14
+ "@mesh-tech/logger": "latest",
14
15
  "@opentelemetry/auto-instrumentations-node": "^0.78.0",
15
16
  "@temporalio/activity": "^1.11.7",
16
17
  "@temporalio/worker": "^1.11.7",
@@ -13,11 +13,15 @@
13
13
  * - Use Date.now(), Math.random(), etc.
14
14
  */
15
15
 
16
+ // `log` carries the activity context and reaches the platform logger the worker
17
+ // installs — never the console (the Mesh app contract, gate 0.4).
18
+ import { log } from "@temporalio/activity";
19
+
16
20
  /**
17
21
  * Generate a greeting message
18
22
  */
19
23
  export async function greet(name: string): Promise<string> {
20
- console.log(`[Activity] Generating greeting for ${name}`);
24
+ log.info("Generating greeting", { name });
21
25
  return `Hello, ${name}!`;
22
26
  }
23
27
 
@@ -25,7 +29,7 @@ export async function greet(name: string): Promise<string> {
25
29
  * Process a task (simulated)
26
30
  */
27
31
  export async function processTask(taskId: string): Promise<string> {
28
- console.log(`[Activity] Processing task ${taskId}`);
32
+ log.info("Processing task", { taskId });
29
33
 
30
34
  // Simulate processing time
31
35
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -40,7 +44,7 @@ export async function sendNotification(
40
44
  userId: string,
41
45
  message: string
42
46
  ): Promise<boolean> {
43
- console.log(`[Activity] Sending notification to ${userId}: ${message}`);
47
+ log.info("Sending notification", { userId, message });
44
48
 
45
49
  // Simulate sending
46
50
  await new Promise((resolve) => setTimeout(resolve, 500));
@@ -16,6 +16,7 @@ import {
16
16
  ValueError,
17
17
  } from "@temporalio/common";
18
18
  import proto from "@temporalio/proto";
19
+ import { logger } from "@mesh-tech/logger";
19
20
 
20
21
  const { temporal } = proto;
21
22
 
@@ -148,9 +149,9 @@ export class EncryptionCodec implements PayloadCodec {
148
149
  export async function createEncryptionCodec(): Promise<EncryptionCodec | undefined> {
149
150
  const encodingKey = process.env.TEMPORAL_ENCODING_KEY;
150
151
  if (!encodingKey) {
151
- console.log("[Encryption] No TEMPORAL_ENCODING_KEY set, payloads will not be encrypted");
152
+ logger.warn("No TEMPORAL_ENCODING_KEY set, payloads will not be encrypted");
152
153
  return undefined;
153
154
  }
154
- console.log("[Encryption] Payload encryption enabled");
155
+ logger.info("Payload encryption enabled");
155
156
  return EncryptionCodec.create(encodingKey);
156
157
  }
@@ -3,8 +3,14 @@
3
3
  */
4
4
 
5
5
  import { createTemporalWorker, resolveWorkflows } from "@mesh-tech/app-kit/temporal";
6
+ import { createLogger } from "@mesh-tech/logger";
6
7
  import * as activities from "./activities.js";
7
8
 
9
+ // The worker process logs through @mesh-tech/logger (the Mesh app contract,
10
+ // gate 0.4). Activities use `log` from @temporalio/activity and workflows `log`
11
+ // from @temporalio/workflow — both route to the Runtime logger app-kit installs.
12
+ const log = createLogger({ service: "{{name}}-worker" });
13
+
8
14
  async function run() {
9
15
  const { worker, cleanup } = await createTemporalWorker({
10
16
  activities,
@@ -19,6 +25,6 @@ async function run() {
19
25
  }
20
26
 
21
27
  run().catch((err) => {
22
- console.error("Worker failed to start:", err);
28
+ log.error({ err }, "Worker failed to start");
23
29
  process.exit(1);
24
30
  });
@@ -11,7 +11,7 @@
11
11
  * - Use proxyActivities() to call activities
12
12
  */
13
13
 
14
- import { proxyActivities, sleep } from "@temporalio/workflow";
14
+ import { log, proxyActivities, sleep } from "@temporalio/workflow";
15
15
  import type * as activities from "./activities.js";
16
16
 
17
17
  // Configure activity retries and timeouts
@@ -43,7 +43,7 @@ export async function taskWorkflow(
43
43
  taskId: string,
44
44
  userId: string
45
45
  ): Promise<{ status: string; taskId: string }> {
46
- console.log(`[Workflow] Processing task ${taskId} for user ${userId}`);
46
+ log.info("Processing task", { taskId, userId });
47
47
 
48
48
  // Process the task
49
49
  const result = await processTask(taskId);
@@ -51,7 +51,7 @@ export async function taskWorkflow(
51
51
  // Wait before completing (demonstrates durable timers)
52
52
  await sleep("2 seconds");
53
53
 
54
- console.log(`[Workflow] Task ${taskId} completed with status: ${result}`);
54
+ log.info("Task completed", { taskId, result });
55
55
 
56
56
  return {
57
57
  status: result,