6 Commits

Author SHA1 Message Date
Space-Banane 0434376b83 Bump version to 2.3.1 2026-05-05 12:48:35 +02:00
Space-Banane 681a3cd6df Add function exec-url command 2026-05-05 12:46:24 +02:00
Space-Banane 544f6bd714 feat(cors): implement CORS management commands and utilities 2026-04-14 22:38:27 +02:00
Space-Banane c912c791fa Added coding docs from shsf to shsf-cli 2026-04-14 18:23:11 +02:00
Space-Banane d9d6871f09 bump version 2026-04-14 18:13:35 +02:00
Space-Banane 121b0429f6 fix(cli): use PUT for trigger updates to match OpenAPI spec 2026-04-14 18:11:13 +02:00
17 changed files with 749 additions and 5 deletions
+2 -1
View File
@@ -2,4 +2,5 @@ node_modules
dist dist
build build
pnpm-lock.yaml pnpm-lock.yaml
testing testing
.codex
+294 -1
View File
@@ -30,6 +30,7 @@ this will check the health, and if not setup, it will prompt you to set up the C
- `shsf delete trigger <functionId> <triggerId>`: Deletes a specific trigger from a function. - `shsf delete trigger <functionId> <triggerId>`: Deletes a specific trigger from a function.
- `shsf get function <id>`: Get details of a specific function by its ID. - `shsf get function <id>`: Get details of a specific function by its ID.
- `shsf get exec-url [--id <id>]`: Get the execution URL for a function. Falls back to `.shsf.json` for the function ID and also prints the alias URL when the function has an `executionAlias`.
- `shsf get namespace <id>`: Get details of a specific namespace by its ID - `shsf get namespace <id>`: Get details of a specific namespace by its ID
- `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function. - `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function.
@@ -81,6 +82,12 @@ You can also create a `.shsf.json` mapping file in the repository root so you do
Command-line flags take precedence over values in `.shsf.json`. Command-line flags take precedence over values in `.shsf.json`.
This mapping can also be used with `shsf get exec-url`, so from a mapped function directory you can run:
```bash
shsf get exec-url
```
## Instructions ## Instructions
Use these commands for when you need to interact with shsf from the command line. Its faster than using the ui for almost all ops. Use these commands for when you need to interact with shsf from the command line. Its faster than using the ui for almost all ops.
@@ -96,6 +103,18 @@ After creating a function and receiving an ID (for example, 81), you can share t
[UI_URL]/functions/[ID] [UI_URL]/functions/[ID]
``` ```
To get the function execution URL from the CLI:
```bash
shsf get exec-url --id 81
```
If the function has an execution alias configured, the CLI also prints the alias form:
```text
[API_URL]/exec/[executionAlias]
```
## Update ## Update
Update with your package manager of choice. ALWAYS pnpm: Update with your package manager of choice. ALWAYS pnpm:
```bash ```bash
@@ -112,4 +131,278 @@ and then replace the old one with the new one using `mv`.
## Missing a feature? ## Missing a feature?
Features might be not documented yet, as this skill document rarely updates. Features might be not documented yet, as this skill document rarely updates.
Use the shsf -h command to see options and features. Use the shsf -h command to see options and features.
CODING DOC:
# SHSF Platform Reference (Agent-Optimized)
## Critical Rules (check before writing any code)
- Python files use `.py`, Go files use `.go` — never mix runtimes
- Go package must be `main`; entry-point must be `main_user()`, never `main()`
- Python entry-point must be `def main(args):`
- **Always** `import json` and call `json.loads(args.get("body", "{}"))` before accessing body fields in Python
- Forbidden filenames: `_runner.py`, `_runner.js`, `init.sh`
- Filenames must never contain `/` or `\` — no subdirectories
- Never hard-code secrets — use environment variables via `os.getenv()`
- Never invent SHSF APIs not documented here
- Never write partial files or placeholder comments
- Only create `requirements.txt` / `go.mod` if dependencies are actually needed
---
## Entry Points
### Python
```python
def main(args):
return {"hello": "world"} # plain dict → 200 JSON
```
### Go
```go
package main
func main_user(args interface{}) (interface{}, error) {
return map[string]string{"hello": "world"}, nil
}
```
---
## The `args` Object
| Field | Type | Notes |
|------------|------------|-----------------------------------------------------------------------|
| `body` | string | Raw JSON string — **must** be parsed with `json.loads()` before use |
| `queries` | dict / map | URL query parameters |
| `route` | string | Sub-path segment after function URL. Default: `"default"` |
| `headers` | dict / map | Lowercased HTTP request headers |
| `raw_body` | bytes | Raw request body (file uploads, binary data) |
| `method` | string | HTTP method (GET, POST, …) |
Always use `.get()` / nil-checks — never assume a field is present.
### Python args example
```python
import json
def main(args):
body = json.loads(args.get("body", "{}")) # always parse first
queries = args.get("queries", {})
route = args.get("route", "default")
name = body.get("name", "stranger")
page = queries.get("page", "1")
return {"greeting": f"Hello {name}", "page": page, "route": route}
```
---
## Response Formats
### Simple 200 JSON (plain dict)
```python
return {"key": "value"}
```
### v2 Envelope (control status, headers, body)
```python
return {
"_shsf": "v2",
"_code": 201, # HTTP status code
"_headers": {"Content-Type": "application/json"}, # optional
"_res": {"created": True, "id": 42} # response body
}
```
### Error response
```python
return {"_shsf": "v2", "_code": 400, "_res": {"error": "missing field 'name'"}}
```
### Redirect (301 / 302)
```python
return {"_shsf": "v2", "_code": 302, "_location": "https://example.com/target"}
```
### HTML response
```python
def main(args):
with open("index.html", "r") as f:
html = f.read()
return {"_shsf": "v2", "_code": 200, "_headers": {"Content-Type": "text/html"}, "_res": html}
```
> **Static HTML shortcut**: if the only file is a single `.html` set as the startup file, SHSF serves it directly without spinning up a runtime.
---
## Routing
`args["route"]` holds the single URL segment after the function base URL (no leading slash, default `"default"`). Only **one** segment is supported.
```python
def main(args):
route = args.get("route", "default")
if route == "register": return handle_register(args)
elif route == "login": return handle_login(args)
elif route == "status": return {"status": "ok"}
else: return {"_shsf": "v2", "_code": 404, "_res": {"error": "route not found"}}
```
---
## Environment Variables
Never hard-code secrets. Define them in the SHSF dashboard.
```python
import os
def main(args):
api_key = os.getenv("MY_API_KEY", "")
if not api_key:
return {"_shsf": "v2", "_code": 500, "_res": {"error": "MY_API_KEY not set"}}
return {"ok": True}
```
Go: `apiKey := os.Getenv("MY_API_KEY")`
---
## Persistent Storage
| Path | Persistence | Use for |
|---------|--------------------------|---------------------------------|
| `/app/` | Persists across calls | Cache, state files |
| `/tmp/` | Wiped on container restart | Truly temporary scratch work |
**WARNING**: SHSF may restart or update containers, which recreates all of it, even the `/app/` directory. For critical data, use `_db_com` instead.
```python
import json, os
CACHE = "/app/cache.json"
def main(args):
data = json.load(open(CACHE)) if os.path.exists(CACHE) else {}
data["hits"] = data.get("hits", 0) + 1
json.dump(data, open(CACHE, "w"))
return {"hits": data["hits"]}
```
### Redis (shared key-value, fast)
```python
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
def main(args):
r.incr("counter")
return {"counter": int(r.get("counter"))}
```
---
## Database (`_db_com`) — Python
`_db_com.py` is auto-provisioned. Add `requests` to `requirements.txt`.
```python
from _db_com import database
from datetime import datetime, timedelta
db = database()
def main(args):
db.create_storage("my_app", purpose="application data") # idempotent, safe every call
db.set("my_app", "username", "alice") # write
expires = (datetime.utcnow() + timedelta(hours=1)).isoformat()
db.set("my_app", "session", "tok_abc", expires_at=expires) # write with TTL
username = db.get("my_app", "username") # read (None if missing)
exists = db.exists("my_app", "username") # existence check
items = db.list_items("my_app") # list all keys
db.delete_item("my_app", "username") # delete
return {"username": username, "items": items}
```
### Go `dbcom`
```go
package main
import "myfunction/dbcom"
func main_user(args interface{}) (interface{}, error) {
db := dbcom.New()
if _, err := db.Set("my-storage", "key", "value", nil); err != nil {
return nil, err
}
value, err := db.Get("my-storage", "key")
if err != nil {
return nil, err
}
return map[string]interface{}{"value": value}, nil
}
```
---
## File Uploads / Raw Body
```python
def main(args):
raw = args.get("raw_body")
if raw is None:
return {"_shsf": "v2", "_code": 400, "_res": {"error": "no body provided"}}
if isinstance(raw, str):
raw = raw.encode("latin-1")
with open("/app/upload.bin", "wb") as f:
f.write(raw)
return {"_shsf": "v2", "_code": 200, "_res": {"saved": True}}
```
---
## Secure Header (`x-secure-header`)
When the secure-header feature is enabled, SHSF validates the token **before** invoking your function — no need to re-validate. Read it only for logging:
```python
def main(args):
token = args.get("headers", {}).get("x-secure-header", "")
return {"authenticated": True, "token_preview": token[:4] + ""}
```
---
## Dependency Files
| Runtime | File | Notes |
|---------|-------------------|------------------------------------------------|
| Python | `requirements.txt`| pip-installed before first run |
| Go | `go.mod`+`go.sum` | Module deps, auto-downloaded |
Only create these files if you have actual dependencies.
### Python `requirements.txt`
```
requests==2.31.0
beautifulsoup4==4.12.2
```
### Go `go.mod`
```
module myfunction
go 1.23
require (
github.com/google/uuid v1.3.0
)
```
Supported Go versions: `1.20`, `1.21`, `1.22`, `1.23`
+16
View File
@@ -46,6 +46,22 @@ Quickly see if the system is up and running:
shsf health shsf health
``` ```
### 🔗 Execution URL
Get a function's execution URL by ID:
```bash
shsf get exec-url --id <id>
```
If your repo has a `.shsf.json` mapping with an `id`, you can omit `--id`:
```bash
shsf get exec-url
```
The command prints the standard execution URL and, when available, the alias URL based on the function's `executionAlias`.
### 🏗️ Local Development ### 🏗️ Local Development
If you're contributing or running from source: If you're contributing or running from source:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "shsf-cli", "name": "shsf-cli",
"version": "2.2.9", "version": "2.3.1",
"description": "", "description": "",
"type": "module", "type": "module",
"files": [ "files": [
+1 -1
View File
@@ -48,5 +48,5 @@ describe('Command Loading', () => {
console.log(`✓ Validated command: ${definition.name} (${path.relative(commandsDir, file)})`); console.log(`✓ Validated command: ${definition.name} (${path.relative(commandsDir, file)})`);
} }
}); }, 20000);
}); });
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { parseCorsOriginsOption } from "../utils/cors.js";
describe("parseCorsOriginsOption", () => {
it("returns undefined when no value is provided", () => {
expect(parseCorsOriginsOption(undefined)).toEqual({});
});
it("parses, normalizes, and deduplicates URLs", () => {
const parsed = parseCorsOriginsOption([
"https://example.com",
"https://example.com/path",
"http://localhost:3000,http://localhost:3000/",
]);
expect(parsed).toEqual({
corsOrigins: ["https://example.com", "http://localhost:3000"],
});
});
it("returns an error for invalid URLs", () => {
const parsed = parseCorsOriginsOption(["not-a-url"]);
expect(parsed.error).toMatch("Invalid CORS origin URL");
});
it("returns an error for unsupported protocols", () => {
const parsed = parseCorsOriginsOption(["ftp://example.com"]);
expect(parsed.error).toMatch("Only http and https are allowed");
});
});
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { buildFunctionExecUrls } from "../utils/function_exec_url.js";
describe("buildFunctionExecUrls", () => {
it("builds the ID-based execution URL and trims trailing slashes", () => {
expect(
buildFunctionExecUrls("https://example.com/", {
namespaceId: 4,
executionId: "abc-123",
}),
).toEqual({
executionUrl: "https://example.com/api/exec/4/abc-123",
});
});
it("includes the alias URL when the function has an execution alias", () => {
expect(
buildFunctionExecUrls("https://example.com", {
namespaceId: 7,
executionId: "uuid-456",
executionAlias: "friendly-name",
}),
).toEqual({
executionUrl: "https://example.com/api/exec/7/uuid-456",
aliasUrl: "https://example.com/exec/friendly-name",
});
});
it("throws when required execution URL fields are missing", () => {
expect(() =>
buildFunctionExecUrls("https://example.com", {
executionAlias: "friendly-name",
}),
).toThrow("Function response is missing execution URL fields.");
});
});
+41
View File
@@ -0,0 +1,41 @@
import chalk from "chalk";
import {
getFunctionCorsOrigins,
handleCorsCommandError,
resolveFunctionId,
setFunctionCorsOrigins,
validateSingleOrigin,
} from "../../utils/cors_commands.js";
export const addCorsDefinition = {
name: "add <origin>",
description: "Add a CORS allowlist origin (must start with http:// or https://).",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (origin: string, options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
const validated = validateSingleOrigin(origin);
if (validated.error) {
console.error(`${chalk.red("✗")} ${validated.error}`);
return;
}
try {
const currentOrigins = await getFunctionCorsOrigins(functionId);
const normalizedOrigin = validated.origin as string;
if (currentOrigins.includes(normalizedOrigin)) {
console.log(`${chalk.yellow("!")} Origin ${chalk.cyan(normalizedOrigin)} already exists for function ${chalk.cyan(functionId)}.`);
return;
}
const updatedOrigins = [...currentOrigins, normalizedOrigin];
await setFunctionCorsOrigins(functionId, updatedOrigins);
console.log(`${chalk.green("✓")} Added CORS origin ${chalk.cyan(normalizedOrigin)} to function ${chalk.cyan(functionId)}.`);
} catch (error: any) {
handleCorsCommandError(error, "add CORS origin");
}
},
};
+30
View File
@@ -0,0 +1,30 @@
import chalk from "chalk";
import {
getFunctionCorsOrigins,
handleCorsCommandError,
resolveFunctionId,
setFunctionCorsOrigins,
} from "../../utils/cors_commands.js";
export const clearCorsDefinition = {
name: "clear",
description: "Clear all CORS allowlist origins for a function.",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
try {
const currentOrigins = await getFunctionCorsOrigins(functionId);
if (currentOrigins.length === 0) {
console.log(`${chalk.yellow("!")} CORS allowlist is already empty for function ${chalk.cyan(functionId)}.`);
return;
}
await setFunctionCorsOrigins(functionId, []);
console.log(`${chalk.green("✓")} Cleared all CORS origins for function ${chalk.cyan(functionId)}.`);
} catch (error: any) {
handleCorsCommandError(error, "clear CORS origins");
}
},
};
+27
View File
@@ -0,0 +1,27 @@
import chalk from "chalk";
import { getFunctionCorsOrigins, handleCorsCommandError, resolveFunctionId } from "../../utils/cors_commands.js";
export const listCorsDefinition = {
name: "list",
description: "List CORS allowlist origins for a function.",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
try {
const origins = await getFunctionCorsOrigins(functionId);
if (origins.length === 0) {
console.log(`${chalk.yellow("!")} No CORS allowlist origins configured for function ${chalk.cyan(functionId)}.`);
return;
}
console.log(`${chalk.blue("CORS Origins")} for function ${chalk.cyan(functionId)}:`);
origins.forEach((origin) => {
console.log(`- ${chalk.cyan(origin)}`);
});
} catch (error: any) {
handleCorsCommandError(error, "list CORS origins");
}
},
};
+41
View File
@@ -0,0 +1,41 @@
import chalk from "chalk";
import {
getFunctionCorsOrigins,
handleCorsCommandError,
resolveFunctionId,
setFunctionCorsOrigins,
validateSingleOrigin,
} from "../../utils/cors_commands.js";
export const removeCorsDefinition = {
name: "remove <origin>",
description: "Remove a CORS allowlist origin (must start with http:// or https://).",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (origin: string, options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
const validated = validateSingleOrigin(origin);
if (validated.error) {
console.error(`${chalk.red("✗")} ${validated.error}`);
return;
}
try {
const currentOrigins = await getFunctionCorsOrigins(functionId);
const normalizedOrigin = validated.origin as string;
if (!currentOrigins.includes(normalizedOrigin)) {
console.log(`${chalk.yellow("!")} Origin ${chalk.cyan(normalizedOrigin)} is not set for function ${chalk.cyan(functionId)}.`);
return;
}
const updatedOrigins = currentOrigins.filter((existingOrigin) => existingOrigin !== normalizedOrigin);
await setFunctionCorsOrigins(functionId, updatedOrigins);
console.log(`${chalk.green("✓")} Removed CORS origin ${chalk.cyan(normalizedOrigin)} from function ${chalk.cyan(functionId)}.`);
} catch (error: any) {
handleCorsCommandError(error, "remove CORS origin");
}
},
};
+63
View File
@@ -0,0 +1,63 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { loadConfig } from "../../config.js";
import { resolveFunctionId } from "../../utils/function_commands.js";
import { buildFunctionExecUrls } from "../../utils/function_exec_url.js";
export const getExecUrlDefinition = {
name: "exec-url",
description: "Get a function execution URL by its ID.",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
await getExecUrl(functionId);
},
};
async function getExecUrl(functionId: string): Promise<void> {
const client = await getApiClient();
const config = await loadConfig();
try {
const response = await client.get(`/api/function/${functionId}`);
const functionData = response.data?.data;
if (!functionData) {
console.error(`${chalk.red("✗")} Unexpected response from server.`);
return;
}
if (!functionData.allow_http) {
console.error(`${chalk.red("✗")} HTTP execution is not allowed for function ${chalk.yellow(functionId)}.`);
return;
}
const urls = buildFunctionExecUrls(config.SHSF_INSTANCE, functionData);
console.log(`${chalk.blue("Execution URL")} for function ${chalk.cyan(functionId)}:`);
console.log(chalk.bgGreen.black(` ${urls.executionUrl} `));
if (urls.aliasUrl) {
console.log(`${chalk.blue("Alias URL")} for function ${chalk.cyan(functionId)}:`);
console.log(chalk.bgGreen.black(` ${urls.aliasUrl} `));
}
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
console.error(`${chalk.red("✗")} Function with ID ${chalk.yellow(functionId)} not found.`);
} else {
console.error(`${chalk.red("✗")} Failed to fetch function details.`);
console.error(`Status Code: ${chalk.red(error.response.status)}`);
console.error(
`Message: ${chalk.yellow(error.response.data?.message || "Unknown error from server")}`,
);
}
} else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
} else {
console.error(`${chalk.red("✗")} Error: ${chalk.yellow(error.message)}`);
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ export const updateTriggerDefinition = {
const client = await getApiClient(); const client = await getApiClient();
try { try {
const response = await client.patch(`/api/functions/${functionId}/triggers/${triggerId}`, data); const response = await client.put(`/api/functions/${functionId}/triggers/${triggerId}`, data);
if (response.status === 200) { if (response.status === 200) {
console.log( console.log(
+42
View File
@@ -0,0 +1,42 @@
export function parseCorsOriginsOption(rawValue: unknown): { corsOrigins?: string[]; error?: string } {
if (rawValue === undefined || rawValue === null) {
return {};
}
const tokens = Array.isArray(rawValue) ? rawValue : [rawValue];
const splitValues = tokens
.map((value) => String(value).trim())
.flatMap((value) => value.split(","))
.map((value) => value.trim())
.filter(Boolean);
if (splitValues.length === 0) {
return { error: "No valid values were provided for --cors-origins." };
}
const uniqueOrigins: string[] = [];
const seen = new Set<string>();
for (const origin of splitValues) {
let parsed: URL;
try {
parsed = new URL(origin);
} catch {
return { error: `Invalid CORS origin URL: ${origin}` };
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { error: `Invalid CORS origin protocol for ${origin}. Only http and https are allowed.` };
}
const normalizedOrigin = parsed.origin;
if (!seen.has(normalizedOrigin)) {
seen.add(normalizedOrigin);
uniqueOrigins.push(normalizedOrigin);
}
}
return { corsOrigins: uniqueOrigins };
}
+70
View File
@@ -0,0 +1,70 @@
import chalk from "chalk";
import { getApiClient } from "../api.js";
import { parseCorsOriginsOption } from "./cors.js";
import { resolveFunctionId } from "./function_commands.js";
export { resolveFunctionId } from "./function_commands.js";
export function validateSingleOrigin(origin: string): { origin?: string; error?: string } {
const parsed = parseCorsOriginsOption(origin);
if (parsed.error) return { error: parsed.error };
const normalized = parsed.corsOrigins?.[0];
if (!normalized) {
return { error: "No valid origin provided." };
}
return { origin: normalized };
}
export function parseCorsOriginsString(corsOriginsRaw: unknown): string[] {
if (typeof corsOriginsRaw !== "string") {
return [];
}
return corsOriginsRaw
.split(",")
.map((value) => value.trim())
.filter(Boolean);
}
export async function getFunctionCorsOrigins(functionId: string): Promise<string[]> {
const client = await getApiClient();
const response = await client.get(`/api/function/${functionId}/cors-origins`);
const rawValue =
response.data?.cors_origins ??
response.data?.data?.cors_origins ??
response.data?.data ??
"";
return parseCorsOriginsString(rawValue);
}
export async function setFunctionCorsOrigins(functionId: string, origins: string[]): Promise<void> {
const client = await getApiClient();
await client.patch(`/api/function/${functionId}/cors-origins`, {
cors_origins: origins.join(","),
});
}
export function handleCorsCommandError(error: any, action: string) {
if (error.response?.status === 404) {
console.error(`${chalk.red("✗")} Function not found.`);
return;
}
if (error.response) {
console.error(
`${chalk.red("✗")} Failed to ${action}: ${chalk.yellow(error.response.data?.message || "Unknown error")}`,
);
return;
}
if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
return;
}
console.error(`${chalk.red("✗")} Error: ${error.message}`);
}
+19
View File
@@ -0,0 +1,19 @@
import chalk from "chalk";
import { readMappingFile } from "./push_helpers.js";
export function resolveFunctionId(options: { id?: string }): string | null {
if (options.id) {
return options.id;
}
const mapping = readMappingFile();
if (mapping?.id) {
console.log(chalk.blue(`Using mapped id ${mapping.id} from .shsf.json`));
return mapping.id;
}
console.error(
`${chalk.red("✗")} Function ID is required. Use ${chalk.cyan("--id <id>")} or provide an ${chalk.cyan(".shsf.json")} mapping with an ${chalk.cyan("id")}.`,
);
return null;
}
+35
View File
@@ -0,0 +1,35 @@
export interface FunctionExecUrlInput {
namespaceId?: number | string;
executionId?: string;
executionAlias?: string | null;
}
export interface FunctionExecUrls {
executionUrl: string;
aliasUrl?: string;
}
function normalizeBaseUrl(instanceUrl: string): string {
return instanceUrl.replace(/\/+$/, "");
}
export function buildFunctionExecUrls(
instanceUrl: string,
functionData: FunctionExecUrlInput,
): FunctionExecUrls {
const baseUrl = normalizeBaseUrl(instanceUrl);
if (functionData.namespaceId === undefined || !functionData.executionId) {
throw new Error("Function response is missing execution URL fields.");
}
const urls: FunctionExecUrls = {
executionUrl: `${baseUrl}/api/exec/${functionData.namespaceId}/${functionData.executionId}`,
};
if (functionData.executionAlias) {
urls.aliasUrl = `${baseUrl}/exec/${functionData.executionAlias}`;
}
return urls;
}