@mesh-tech/mesh-cli 0.1.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.d.ts +16 -0
- package/dist/bin/mesh.d.ts.map +1 -0
- package/dist/bin/mesh.js +40 -0
- package/dist/bin/mesh.js.map +1 -0
- package/dist/src/commands/create-app.d.ts +10 -0
- package/dist/src/commands/create-app.d.ts.map +1 -0
- package/dist/src/commands/create-app.js +176 -0
- package/dist/src/commands/create-app.js.map +1 -0
- package/dist/src/commands/db/connect.d.ts +13 -0
- package/dist/src/commands/db/connect.d.ts.map +1 -0
- package/dist/src/commands/db/connect.js +68 -0
- package/dist/src/commands/db/connect.js.map +1 -0
- package/dist/src/commands/db/credentials.d.ts +16 -0
- package/dist/src/commands/db/credentials.d.ts.map +1 -0
- package/dist/src/commands/db/credentials.js +56 -0
- package/dist/src/commands/db/credentials.js.map +1 -0
- package/dist/src/commands/db/env.d.ts +15 -0
- package/dist/src/commands/db/env.d.ts.map +1 -0
- package/dist/src/commands/db/env.js +50 -0
- package/dist/src/commands/db/env.js.map +1 -0
- package/dist/src/commands/db/exec.d.ts +54 -0
- package/dist/src/commands/db/exec.d.ts.map +1 -0
- package/dist/src/commands/db/exec.js +331 -0
- package/dist/src/commands/db/exec.js.map +1 -0
- package/dist/src/commands/db/index.d.ts +12 -0
- package/dist/src/commands/db/index.d.ts.map +1 -0
- package/dist/src/commands/db/index.js +62 -0
- package/dist/src/commands/db/index.js.map +1 -0
- package/dist/src/commands/db/psql.d.ts +16 -0
- package/dist/src/commands/db/psql.d.ts.map +1 -0
- package/dist/src/commands/db/psql.js +133 -0
- package/dist/src/commands/db/psql.js.map +1 -0
- package/dist/src/commands/deploy.d.ts +24 -0
- package/dist/src/commands/deploy.d.ts.map +1 -0
- package/dist/src/commands/deploy.js +228 -0
- package/dist/src/commands/deploy.js.map +1 -0
- package/dist/src/commands/dev.d.ts +17 -0
- package/dist/src/commands/dev.d.ts.map +1 -0
- package/dist/src/commands/dev.js +1248 -0
- package/dist/src/commands/dev.js.map +1 -0
- package/dist/src/commands/devbox.d.ts +22 -0
- package/dist/src/commands/devbox.d.ts.map +1 -0
- package/dist/src/commands/devbox.js +463 -0
- package/dist/src/commands/devbox.js.map +1 -0
- package/dist/src/commands/login.d.ts +47 -0
- package/dist/src/commands/login.d.ts.map +1 -0
- package/dist/src/commands/login.js +344 -0
- package/dist/src/commands/login.js.map +1 -0
- package/dist/src/commands/registry.d.ts +20 -0
- package/dist/src/commands/registry.d.ts.map +1 -0
- package/dist/src/commands/registry.js +336 -0
- package/dist/src/commands/registry.js.map +1 -0
- package/dist/src/commands/secrets/exec.d.ts +17 -0
- package/dist/src/commands/secrets/exec.d.ts.map +1 -0
- package/dist/src/commands/secrets/exec.js +132 -0
- package/dist/src/commands/secrets/exec.js.map +1 -0
- package/dist/src/commands/secrets/index.d.ts +10 -0
- package/dist/src/commands/secrets/index.d.ts.map +1 -0
- package/dist/src/commands/secrets/index.js +35 -0
- package/dist/src/commands/secrets/index.js.map +1 -0
- package/dist/src/commands/secrets/set.d.ts +22 -0
- package/dist/src/commands/secrets/set.d.ts.map +1 -0
- package/dist/src/commands/secrets/set.js +234 -0
- package/dist/src/commands/secrets/set.js.map +1 -0
- package/dist/src/commands/stack.d.ts +17 -0
- package/dist/src/commands/stack.d.ts.map +1 -0
- package/dist/src/commands/stack.js +175 -0
- package/dist/src/commands/stack.js.map +1 -0
- package/dist/src/commands/tunnel/index.d.ts +14 -0
- package/dist/src/commands/tunnel/index.d.ts.map +1 -0
- package/dist/src/commands/tunnel/index.js +217 -0
- package/dist/src/commands/tunnel/index.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/utils/bastion.d.ts +43 -0
- package/dist/src/utils/bastion.d.ts.map +1 -0
- package/dist/src/utils/bastion.js +77 -0
- package/dist/src/utils/bastion.js.map +1 -0
- package/dist/src/utils/context.d.ts +29 -0
- package/dist/src/utils/context.d.ts.map +1 -0
- package/dist/src/utils/context.js +132 -0
- package/dist/src/utils/context.js.map +1 -0
- package/dist/src/utils/credentials.d.ts +61 -0
- package/dist/src/utils/credentials.d.ts.map +1 -0
- package/dist/src/utils/credentials.js +145 -0
- package/dist/src/utils/credentials.js.map +1 -0
- package/dist/src/utils/index.d.ts +9 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +9 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/log.d.ts +8 -0
- package/dist/src/utils/log.d.ts.map +1 -0
- package/dist/src/utils/log.js +17 -0
- package/dist/src/utils/log.js.map +1 -0
- package/dist/src/utils/pulumi.d.ts +24 -0
- package/dist/src/utils/pulumi.d.ts.map +1 -0
- package/dist/src/utils/pulumi.js +63 -0
- package/dist/src/utils/pulumi.js.map +1 -0
- package/package.json +50 -0
- package/templates/api-web-db/api/.dockerignore +2 -0
- package/templates/api-web-db/api/Dockerfile +16 -0
- package/templates/api-web-db/api/package.json.hbs +21 -0
- package/templates/api-web-db/api/src/index.ts.hbs +123 -0
- package/templates/api-web-db/api/tsconfig.json +13 -0
- package/templates/api-web-db/package.json.hbs +27 -0
- package/templates/api-web-db/sst.config.ts.hbs +151 -0
- package/templates/api-web-db/tsconfig.json +11 -0
- package/templates/api-web-db/web/.dockerignore +3 -0
- package/templates/api-web-db/web/Dockerfile +17 -0
- package/templates/api-web-db/web/app/lib/db.server.ts +36 -0
- package/templates/api-web-db/web/app/root.tsx +29 -0
- package/templates/api-web-db/web/app/routes/home.tsx.hbs +114 -0
- package/templates/api-web-db/web/app/routes.ts +3 -0
- package/templates/api-web-db/web/package.json.hbs +30 -0
- package/templates/api-web-db/web/prisma/schema.prisma +25 -0
- package/templates/api-web-db/web/prisma.config.ts +18 -0
- package/templates/api-web-db/web/react-router.config.ts +5 -0
- package/templates/api-web-db/web/tsconfig.json +19 -0
- package/templates/api-web-db/web/vite.config.ts +9 -0
- package/templates/temporal-api-worker/README.md.hbs +133 -0
- package/templates/temporal-api-worker/api/.dockerignore +4 -0
- package/templates/temporal-api-worker/api/Dockerfile +13 -0
- package/templates/temporal-api-worker/api/package.json.hbs +23 -0
- package/templates/temporal-api-worker/api/src/encryption-codec.ts +133 -0
- package/templates/temporal-api-worker/api/src/index.ts.hbs +145 -0
- package/templates/temporal-api-worker/api/tsconfig.json +14 -0
- package/templates/temporal-api-worker/package.json.hbs +29 -0
- package/templates/temporal-api-worker/sst.config.ts.hbs +161 -0
- package/templates/temporal-api-worker/tsconfig.json +10 -0
- package/templates/temporal-api-worker/worker/.dockerignore +4 -0
- package/templates/temporal-api-worker/worker/Dockerfile +11 -0
- package/templates/temporal-api-worker/worker/package.json.hbs +25 -0
- package/templates/temporal-api-worker/worker/src/activities.ts.hbs +49 -0
- package/templates/temporal-api-worker/worker/src/bundle-workflows.ts +38 -0
- package/templates/temporal-api-worker/worker/src/encryption-codec.ts +156 -0
- package/templates/temporal-api-worker/worker/src/worker.ts.hbs +93 -0
- package/templates/temporal-api-worker/worker/src/workflows.ts.hbs +60 -0
- package/templates/temporal-api-worker/worker/tsconfig.json +14 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption Codec for Temporal Payload Encryption
|
|
3
|
+
*
|
|
4
|
+
* Implements AES-256-GCM encryption for workflow payloads.
|
|
5
|
+
* Uses TEMPORAL_ENCODING_KEY environment variable.
|
|
6
|
+
*
|
|
7
|
+
* This is a copy of the worker's encryption codec - both need
|
|
8
|
+
* to use the same encryption to communicate.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as crypto from "crypto";
|
|
12
|
+
import {
|
|
13
|
+
PayloadCodec,
|
|
14
|
+
Payload,
|
|
15
|
+
encodingKeys,
|
|
16
|
+
METADATA_ENCODING_KEY,
|
|
17
|
+
} from "@temporalio/common";
|
|
18
|
+
import proto from "@temporalio/proto";
|
|
19
|
+
|
|
20
|
+
const { temporal } = proto;
|
|
21
|
+
|
|
22
|
+
const ENCODING_TYPE = "binary/encrypted";
|
|
23
|
+
const ALGORITHM = "aes-256-gcm";
|
|
24
|
+
const IV_LENGTH = 12;
|
|
25
|
+
const AUTH_TAG_LENGTH = 16;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Derive a 256-bit key from the encoding key using SHA-256
|
|
29
|
+
*/
|
|
30
|
+
async function deriveKey(encodingKey: string): Promise<Buffer> {
|
|
31
|
+
return crypto.createHash("sha256").update(encodingKey).digest();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Encrypt data using AES-256-GCM
|
|
36
|
+
*/
|
|
37
|
+
function encrypt(data: Uint8Array, key: Buffer): Uint8Array {
|
|
38
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
39
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
40
|
+
|
|
41
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
42
|
+
const authTag = cipher.getAuthTag();
|
|
43
|
+
|
|
44
|
+
// Format: IV (12 bytes) + Ciphertext + AuthTag (16 bytes)
|
|
45
|
+
return Buffer.concat([iv, encrypted, authTag]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decrypt data using AES-256-GCM
|
|
50
|
+
*/
|
|
51
|
+
function decrypt(data: Uint8Array, key: Buffer): Uint8Array {
|
|
52
|
+
const iv = data.slice(0, IV_LENGTH);
|
|
53
|
+
const authTag = data.slice(-AUTH_TAG_LENGTH);
|
|
54
|
+
const ciphertext = data.slice(IV_LENGTH, -AUTH_TAG_LENGTH);
|
|
55
|
+
|
|
56
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
57
|
+
decipher.setAuthTag(authTag);
|
|
58
|
+
|
|
59
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Encryption codec implementing Temporal's PayloadCodec interface
|
|
64
|
+
*/
|
|
65
|
+
export class EncryptionCodec implements PayloadCodec {
|
|
66
|
+
private readonly key: Buffer;
|
|
67
|
+
private readonly keyId: string;
|
|
68
|
+
|
|
69
|
+
private constructor(key: Buffer, keyId: string) {
|
|
70
|
+
this.key = key;
|
|
71
|
+
this.keyId = keyId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static async create(encodingKey: string): Promise<EncryptionCodec> {
|
|
75
|
+
const key = await deriveKey(encodingKey);
|
|
76
|
+
// Use first 8 chars of key hash as key ID
|
|
77
|
+
const keyId = crypto
|
|
78
|
+
.createHash("sha256")
|
|
79
|
+
.update(encodingKey)
|
|
80
|
+
.digest("hex")
|
|
81
|
+
.slice(0, 8);
|
|
82
|
+
return new EncryptionCodec(key, keyId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async encode(payloads: Payload[]): Promise<Payload[]> {
|
|
86
|
+
return payloads.map((payload) => {
|
|
87
|
+
// Serialize the payload to bytes
|
|
88
|
+
const bytes = temporal.api.common.v1.Payload.encode(payload).finish();
|
|
89
|
+
|
|
90
|
+
// Encrypt
|
|
91
|
+
const encrypted = encrypt(bytes, this.key);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
metadata: {
|
|
95
|
+
[METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_RAW,
|
|
96
|
+
"encryption-codec": Buffer.from(ENCODING_TYPE),
|
|
97
|
+
"encryption-key-id": Buffer.from(this.keyId),
|
|
98
|
+
},
|
|
99
|
+
data: encrypted,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async decode(payloads: Payload[]): Promise<Payload[]> {
|
|
105
|
+
return payloads.map((payload) => {
|
|
106
|
+
// Check if this payload is encrypted by us
|
|
107
|
+
const encoding = payload.metadata?.["encryption-codec"];
|
|
108
|
+
if (!encoding || Buffer.from(encoding).toString() !== ENCODING_TYPE) {
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Decrypt
|
|
113
|
+
const decrypted = decrypt(payload.data!, this.key);
|
|
114
|
+
|
|
115
|
+
// Deserialize back to Payload
|
|
116
|
+
return temporal.api.common.v1.Payload.decode(decrypted);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create encryption codec from environment variable
|
|
123
|
+
*/
|
|
124
|
+
export async function createEncryptionCodec(): Promise<
|
|
125
|
+
EncryptionCodec | undefined
|
|
126
|
+
> {
|
|
127
|
+
const encodingKey = process.env.TEMPORAL_ENCODING_KEY;
|
|
128
|
+
if (!encodingKey) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return EncryptionCodec.create(encodingKey);
|
|
133
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{titleCase name}} API
|
|
3
|
+
*
|
|
4
|
+
* HTTP API for triggering Temporal workflows.
|
|
5
|
+
*
|
|
6
|
+
* Environment variables:
|
|
7
|
+
* - PORT: Server port (default: 3000)
|
|
8
|
+
* - TEMPORAL_ADDRESS: Temporal server address (default: localhost:7233)
|
|
9
|
+
* - TEMPORAL_NAMESPACE: Temporal namespace (required)
|
|
10
|
+
* - TEMPORAL_TASK_QUEUE: Task queue name (required)
|
|
11
|
+
* - TEMPORAL_ENCODING_KEY: Encoding key for payload encryption (optional)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import express, { Request, Response } from "express";
|
|
15
|
+
import { Client, Connection } from "@temporalio/client";
|
|
16
|
+
import { createEncryptionCodec } from "./encryption-codec.js";
|
|
17
|
+
|
|
18
|
+
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
19
|
+
const TEMPORAL_ADDRESS = process.env.TEMPORAL_ADDRESS ?? "localhost:7233";
|
|
20
|
+
|
|
21
|
+
function requireEnv(name: string): string {
|
|
22
|
+
const value = process.env[name];
|
|
23
|
+
if (!value) {
|
|
24
|
+
throw new Error(`${name} environment variable is required`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TEMPORAL_NAMESPACE = requireEnv("TEMPORAL_NAMESPACE");
|
|
30
|
+
const TEMPORAL_TASK_QUEUE = requireEnv("TEMPORAL_TASK_QUEUE");
|
|
31
|
+
|
|
32
|
+
const app = express();
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
|
|
35
|
+
let client: Client;
|
|
36
|
+
|
|
37
|
+
// Initialize Temporal client
|
|
38
|
+
async function initClient(): Promise<void> {
|
|
39
|
+
console.log(`[API] Connecting to Temporal at ${TEMPORAL_ADDRESS}...`);
|
|
40
|
+
|
|
41
|
+
const connection = await Connection.connect({
|
|
42
|
+
address: TEMPORAL_ADDRESS,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Set up payload encryption if encoding key is available
|
|
46
|
+
const encryptionCodec = await createEncryptionCodec();
|
|
47
|
+
const dataConverter = encryptionCodec
|
|
48
|
+
? { payloadCodecs: [encryptionCodec] }
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
client = new Client({
|
|
52
|
+
connection,
|
|
53
|
+
namespace: TEMPORAL_NAMESPACE,
|
|
54
|
+
dataConverter,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(`[API] Connected. Namespace: ${TEMPORAL_NAMESPACE}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Health check
|
|
61
|
+
app.get("/health", (_req: Request, res: Response) => {
|
|
62
|
+
res.json({ status: "ok" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Trigger greeting workflow
|
|
66
|
+
app.post("/greet", async (req: Request, res: Response) => {
|
|
67
|
+
try {
|
|
68
|
+
const { name } = req.body as { name: string };
|
|
69
|
+
if (!name) {
|
|
70
|
+
res.status(400).json({ error: "name is required" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handle = await client.workflow.start("greetingWorkflow", {
|
|
75
|
+
taskQueue: TEMPORAL_TASK_QUEUE,
|
|
76
|
+
workflowId: `greeting-${Date.now()}`,
|
|
77
|
+
args: [name],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Wait for result (greeting is quick)
|
|
81
|
+
const result = await handle.result();
|
|
82
|
+
res.json({ workflowId: handle.workflowId, result });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("[API] Error starting workflow:", error);
|
|
85
|
+
res.status(500).json({ error: "Failed to start workflow" });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Trigger task workflow
|
|
90
|
+
app.post("/task", async (req: Request, res: Response) => {
|
|
91
|
+
try {
|
|
92
|
+
const { taskId, userId } = req.body as { taskId: string; userId: string };
|
|
93
|
+
if (!taskId || !userId) {
|
|
94
|
+
res.status(400).json({ error: "taskId and userId are required" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handle = await client.workflow.start("taskWorkflow", {
|
|
99
|
+
taskQueue: TEMPORAL_TASK_QUEUE,
|
|
100
|
+
workflowId: `task-${taskId}`,
|
|
101
|
+
args: [taskId, userId],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Don't wait for result - return workflow ID immediately
|
|
105
|
+
res.json({
|
|
106
|
+
workflowId: handle.workflowId,
|
|
107
|
+
status: "started",
|
|
108
|
+
message: "Workflow started. Check Temporal Web UI for progress.",
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("[API] Error starting workflow:", error);
|
|
112
|
+
res.status(500).json({ error: "Failed to start workflow" });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Get workflow status
|
|
117
|
+
app.get("/workflow/:workflowId", async (req: Request, res: Response) => {
|
|
118
|
+
try {
|
|
119
|
+
const { workflowId } = req.params;
|
|
120
|
+
const handle = client.workflow.getHandle(workflowId);
|
|
121
|
+
const describe = await handle.describe();
|
|
122
|
+
|
|
123
|
+
res.json({
|
|
124
|
+
workflowId,
|
|
125
|
+
status: describe.status.name,
|
|
126
|
+
startTime: describe.startTime,
|
|
127
|
+
closeTime: describe.closeTime,
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("[API] Error getting workflow:", error);
|
|
131
|
+
res.status(500).json({ error: "Failed to get workflow status" });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Start server
|
|
136
|
+
initClient()
|
|
137
|
+
.then(() => {
|
|
138
|
+
app.listen(PORT, "0.0.0.0", () => {
|
|
139
|
+
console.log(`[API] {{titleCase name}} API listening on port ${PORT}`);
|
|
140
|
+
});
|
|
141
|
+
})
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
console.error("[API] Failed to initialize:", err);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"sst:patch": "patch-sst-pulumi .",
|
|
9
|
+
"dev": "pnpm run sst:patch && sst dev",
|
|
10
|
+
"deploy": "pnpm run sst:patch && sst deploy",
|
|
11
|
+
"deploy:dev": "pnpm run sst:patch && sst deploy --stage dev",
|
|
12
|
+
"deploy:prod": "pnpm run sst:patch && sst deploy --stage prod",
|
|
13
|
+
"remove": "sst remove"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@mesh-tech/infra-components": "workspace:*",
|
|
17
|
+
"@pulumi/aws": "6.66.2",
|
|
18
|
+
"@pulumi/docker-build": "^0.0.15",
|
|
19
|
+
"@pulumi/eks": "4.2.0",
|
|
20
|
+
"@pulumi/kubernetes": "4.24.1",
|
|
21
|
+
"@pulumi/pulumi": "3.214.1",
|
|
22
|
+
"@pulumi/random": "4.18.2",
|
|
23
|
+
"sst": "^3.3.61"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@aws-sdk/credential-providers": "^3.964.0",
|
|
27
|
+
"typescript": "^5.7.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/// <reference path="./.sst/platform/config.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* {{titleCase name}} - Temporal Workflow Application
|
|
5
|
+
*
|
|
6
|
+
* This app demonstrates Temporal workflow orchestration with:
|
|
7
|
+
* - AppEnvironment with temporal: true (namespace provisioning, encoding key)
|
|
8
|
+
* - TemporalWorker component (auto-configured env vars, no Service/Ingress)
|
|
9
|
+
* - API Service for triggering workflows
|
|
10
|
+
*
|
|
11
|
+
* Dev mode:
|
|
12
|
+
* 1. Run: pnpm mesh dev (auto-starts tunnel to Temporal + DB)
|
|
13
|
+
* 2. Start worker manually in SST dev UI
|
|
14
|
+
*
|
|
15
|
+
* Deploy:
|
|
16
|
+
* pnpm deploy --stage dev
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export default $config({
|
|
20
|
+
app(input) {
|
|
21
|
+
return {
|
|
22
|
+
name: "{{name}}",
|
|
23
|
+
removal: input?.stage === "prod" ? "retain" : "remove",
|
|
24
|
+
protect: ["prod"].includes(input?.stage ?? ""),
|
|
25
|
+
home: "aws",
|
|
26
|
+
providers: {
|
|
27
|
+
aws: {
|
|
28
|
+
region: "us-east-2",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
async run() {
|
|
34
|
+
// Dynamic imports (SST requirement for non-SST packages)
|
|
35
|
+
const { mesh } = await import("@mesh-tech/infra-components");
|
|
36
|
+
const { config, getPlatform } = await import("../config.js");
|
|
37
|
+
|
|
38
|
+
const stage = $app.stage;
|
|
39
|
+
const tenant = config.tenant;
|
|
40
|
+
const platform = getPlatform(stage);
|
|
41
|
+
|
|
42
|
+
// =========================================================================
|
|
43
|
+
// AppEnvironment with Temporal enabled
|
|
44
|
+
// =========================================================================
|
|
45
|
+
// This creates:
|
|
46
|
+
// - Kubernetes namespace (deploy mode)
|
|
47
|
+
// - Temporal namespace (via TemporalNamespaceProvisioner Lambda)
|
|
48
|
+
// - Encoding key K8s Secret for payload encryption
|
|
49
|
+
// - Codec server deployment (for Web UI decryption)
|
|
50
|
+
// - Unified dev tunnel (mesh tunnel dev) for Temporal + DB access
|
|
51
|
+
|
|
52
|
+
const env = new mesh.apps.AppEnvironment("env", {
|
|
53
|
+
tenant,
|
|
54
|
+
platform,
|
|
55
|
+
stage,
|
|
56
|
+
appName: "{{name}}",
|
|
57
|
+
dev: $dev,
|
|
58
|
+
namespace: !$dev,
|
|
59
|
+
|
|
60
|
+
// Enable Temporal support
|
|
61
|
+
temporal: true,
|
|
62
|
+
DevCommand: sst.x.DevCommand,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// =========================================================================
|
|
66
|
+
// Temporal Worker
|
|
67
|
+
// =========================================================================
|
|
68
|
+
// TemporalWorker automatically handles:
|
|
69
|
+
// - Temporal environment variables (ADDRESS, NAMESPACE, TASK_QUEUE)
|
|
70
|
+
// - Encoding key secret mounting for payload encryption
|
|
71
|
+
// - No Service/Ingress (workers don't serve HTTP)
|
|
72
|
+
// - Dev mode: doesn't autostart (waits for namespace provisioning)
|
|
73
|
+
|
|
74
|
+
const worker = new mesh.apps.TemporalWorker(
|
|
75
|
+
"worker",
|
|
76
|
+
{
|
|
77
|
+
env,
|
|
78
|
+
rootDir: $cli.paths.root,
|
|
79
|
+
src: "./worker",
|
|
80
|
+
devCommand: ["pnpm", "dev"],
|
|
81
|
+
|
|
82
|
+
// Task queue defaults to worker name ("worker")
|
|
83
|
+
taskQueue: "{{name}}",
|
|
84
|
+
|
|
85
|
+
environment: {
|
|
86
|
+
NODE_ENV: $dev ? "development" : "production",
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
replicas: 1,
|
|
90
|
+
resources: {
|
|
91
|
+
cpu: "100m",
|
|
92
|
+
memory: "256Mi",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
providers: env.providers,
|
|
97
|
+
parent: env,
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// API Service (triggers workflows)
|
|
103
|
+
// =========================================================================
|
|
104
|
+
const api = new mesh.apps.Service(
|
|
105
|
+
"api",
|
|
106
|
+
{
|
|
107
|
+
dev: $dev,
|
|
108
|
+
tenant,
|
|
109
|
+
stage,
|
|
110
|
+
appName: "{{name}}",
|
|
111
|
+
namespace: env.namespace!,
|
|
112
|
+
rootDir: $cli.paths.root,
|
|
113
|
+
src: "./api",
|
|
114
|
+
port: 3000,
|
|
115
|
+
devCommand: ["pnpm", "dev"],
|
|
116
|
+
|
|
117
|
+
environment: {
|
|
118
|
+
NODE_ENV: $dev ? "development" : "production",
|
|
119
|
+
TEMPORAL_ADDRESS: env.temporal!.endpoint.apply((e) =>
|
|
120
|
+
$dev ? "localhost:7233" : `${e}:7233`,
|
|
121
|
+
),
|
|
122
|
+
TEMPORAL_NAMESPACE: env.temporal!.namespace,
|
|
123
|
+
TEMPORAL_TASK_QUEUE: "{{name}}",
|
|
124
|
+
// Dev mode: inject encoding key directly
|
|
125
|
+
...($dev && env.temporal!.devEncodingKey
|
|
126
|
+
? { TEMPORAL_ENCODING_KEY: env.temporal!.devEncodingKey }
|
|
127
|
+
: {}),
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Deploy mode: inject encoding key from K8s secret
|
|
131
|
+
envFromSecrets: $dev ? [] : [env.temporal!.encodingKeySecretName],
|
|
132
|
+
|
|
133
|
+
ingress: env.buildIngress({
|
|
134
|
+
subdomain: "{{name}}-api",
|
|
135
|
+
healthCheckPath: "/health",
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
replicas: 1,
|
|
139
|
+
resources: {
|
|
140
|
+
cpu: "100m",
|
|
141
|
+
memory: "128Mi",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
providers: env.providers,
|
|
146
|
+
parent: env,
|
|
147
|
+
dependsOn: [worker],
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
namespace: env.namespace,
|
|
153
|
+
temporalNamespace: env.temporal?.namespace,
|
|
154
|
+
temporalEndpoint: env.temporal?.endpoint,
|
|
155
|
+
encodingKeySecret: env.temporal?.encodingKeySecretName,
|
|
156
|
+
codecServerUrl: env.temporal?.codecServerUrl,
|
|
157
|
+
workerTaskQueue: worker.taskQueue,
|
|
158
|
+
apiUrl: api.url,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{tenant}}/{{name}}-worker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/worker.ts",
|
|
8
|
+
"start": "node dist/worker.js",
|
|
9
|
+
"build": "tsc && npm run bundle:workflows",
|
|
10
|
+
"bundle:workflows": "tsx src/bundle-workflows.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@temporalio/activity": "^1.11.7",
|
|
14
|
+
"@temporalio/client": "^1.11.7",
|
|
15
|
+
"@temporalio/common": "^1.11.7",
|
|
16
|
+
"@temporalio/proto": "^1.11.7",
|
|
17
|
+
"@temporalio/worker": "^1.11.7",
|
|
18
|
+
"@temporalio/workflow": "^1.11.7"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"tsx": "^4.0.0",
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{titleCase name}} Activities
|
|
3
|
+
*
|
|
4
|
+
* Activities are the building blocks of workflows. They handle
|
|
5
|
+
* non-deterministic operations like API calls, database queries,
|
|
6
|
+
* and external service interactions.
|
|
7
|
+
*
|
|
8
|
+
* Activities can:
|
|
9
|
+
* - Make HTTP requests
|
|
10
|
+
* - Query databases
|
|
11
|
+
* - Send emails/notifications
|
|
12
|
+
* - Access the file system
|
|
13
|
+
* - Use Date.now(), Math.random(), etc.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a greeting message
|
|
18
|
+
*/
|
|
19
|
+
export async function greet(name: string): Promise<string> {
|
|
20
|
+
console.log(`[Activity] Generating greeting for ${name}`);
|
|
21
|
+
return `Hello, ${name}!`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process a task (simulated)
|
|
26
|
+
*/
|
|
27
|
+
export async function processTask(taskId: string): Promise<string> {
|
|
28
|
+
console.log(`[Activity] Processing task ${taskId}`);
|
|
29
|
+
|
|
30
|
+
// Simulate processing time
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
32
|
+
|
|
33
|
+
return "completed";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send a notification (simulated)
|
|
38
|
+
*/
|
|
39
|
+
export async function sendNotification(
|
|
40
|
+
userId: string,
|
|
41
|
+
message: string
|
|
42
|
+
): Promise<boolean> {
|
|
43
|
+
console.log(`[Activity] Sending notification to ${userId}: ${message}`);
|
|
44
|
+
|
|
45
|
+
// Simulate sending
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
47
|
+
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle Workflows for Production
|
|
3
|
+
*
|
|
4
|
+
* Pre-bundles workflows to avoid runtime bundling in production.
|
|
5
|
+
* Run with: npm run bundle:workflows
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { bundleWorkflowCode } from "@temporalio/worker";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
async function bundle() {
|
|
17
|
+
console.log("[Bundle] Bundling workflows...");
|
|
18
|
+
|
|
19
|
+
const bundle = await bundleWorkflowCode({
|
|
20
|
+
workflowsPath: path.join(__dirname, "workflows.ts"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const outPath = path.join(__dirname, "..", "dist", "workflows-bundle.js");
|
|
24
|
+
|
|
25
|
+
// Ensure dist directory exists
|
|
26
|
+
const distDir = path.dirname(outPath);
|
|
27
|
+
if (!fs.existsSync(distDir)) {
|
|
28
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fs.writeFileSync(outPath, bundle.code);
|
|
32
|
+
console.log(`[Bundle] Wrote bundle to ${outPath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
bundle().catch((err) => {
|
|
36
|
+
console.error("[Bundle] Failed:", err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|