@telnyx/edge-runtime 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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # `@telnyx/edge-runtime`
2
+
3
+ Runtime SDK for Telnyx Edge Compute.
@@ -0,0 +1,51 @@
1
+ import type { Env } from "./env.js";
2
+ interface RuntimeBindingEntry {
3
+ binding: string;
4
+ type: "telnyx" | "secrets" | "actor";
5
+ actor_type?: string;
6
+ secret_name?: string;
7
+ }
8
+ /**
9
+ * Reads secrets declared in your `[[secrets]]` blocks.
10
+ *
11
+ * `handle` is the `binding` field — e.g. `env.SECRETS.get("GREETING")`.
12
+ * Throws `SecretsNotConfiguredError` when no `[[secrets]]` were declared,
13
+ * `SecretsError` for any other failure.
14
+ */
15
+ export interface Secrets {
16
+ get(handle: string): Promise<string>;
17
+ }
18
+ export declare class SecretsError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ export declare class SecretsNotConfiguredError extends SecretsError {
22
+ constructor(message: string);
23
+ }
24
+ /**
25
+ * Build the `env` object from the runtime-stamped environment variables.
26
+ *
27
+ * Returns an env with:
28
+ * - `env.<telnyx-binding>` — pre-configured Telnyx client, attached
29
+ * under the handle declared by `[telnyx] binding = "..."`.
30
+ * - `env.SECRETS` — `.get(handle)` reads a secret declared in a
31
+ * `[[secrets]]` block.
32
+ *
33
+ * Missing or malformed config yields an empty env rather than throwing,
34
+ * so module load can't crash on a misconfigured deploy. Calls to absent
35
+ * bindings fail at the call site with a standard `undefined`-access error.
36
+ */
37
+ export declare function buildEnv(): Env;
38
+ declare function loadRuntimeBindings(): RuntimeBindingEntry[];
39
+ declare function resolveSecretsBindingName(): string;
40
+ export declare const env: Env;
41
+ /** Test seam. Not part of the public API. */
42
+ export declare const __internal: {
43
+ DEFAULT_TELNYX_BASE_URL: string;
44
+ TELNYX_API_VERSION_PREFIX: string;
45
+ DEFAULT_DAPR_HTTP_PORT: string;
46
+ PLACEHOLDER_API_KEY: string;
47
+ loadRuntimeBindings: typeof loadRuntimeBindings;
48
+ resolveSecretsBindingName: typeof resolveSecretsBindingName;
49
+ };
50
+ export {};
51
+ //# sourceMappingURL=build-env.d.ts.map
@@ -0,0 +1,207 @@
1
+ // Constructs the `env` object you use in your handler:
2
+ //
3
+ // import { env } from "@telnyx/edge-runtime";
4
+ //
5
+ // export default {
6
+ // async fetch(req: Request): Promise<Response> {
7
+ // const balance = await env.TELNYX_CLIENT.balance.retrieve();
8
+ // const greeting = await env.SECRETS.get("GREETING");
9
+ // // ...
10
+ // },
11
+ // };
12
+ //
13
+ // `buildEnv()` is also exported for tests that need to construct an env
14
+ // with overridden process.env values.
15
+ import Telnyx from "telnyx";
16
+ const DEFAULT_TELNYX_BASE_URL = "http://localhost:3601";
17
+ const TELNYX_API_VERSION_PREFIX = "/v2";
18
+ const DEFAULT_DAPR_HTTP_PORT = "3500";
19
+ // Non-empty placeholder so the Telnyx client attaches an Authorization
20
+ // header. The real bearer is injected by the runtime before the request
21
+ // leaves the pod; your code never holds an API key.
22
+ const PLACEHOLDER_API_KEY = "auth-proxy-will-replace-this";
23
+ export class SecretsError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "SecretsError";
27
+ }
28
+ }
29
+ export class SecretsNotConfiguredError extends SecretsError {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "SecretsNotConfiguredError";
33
+ }
34
+ }
35
+ class SecretsImpl {
36
+ daprUrlRoot;
37
+ bindingName;
38
+ handleToSecretName;
39
+ constructor(daprUrlRoot, bindingName, handleToSecretName) {
40
+ this.daprUrlRoot = daprUrlRoot;
41
+ this.bindingName = bindingName;
42
+ this.handleToSecretName = handleToSecretName;
43
+ }
44
+ async get(handle) {
45
+ if (!this.bindingName) {
46
+ throw new SecretsNotConfiguredError("env.SECRETS.get(...) requires at least one [[secrets]] block in telnyx.toml.");
47
+ }
48
+ // Fall back to the handle itself if it wasn't declared with an
49
+ // explicit `name`, so [[secrets]] binding="FOO" (no name) works.
50
+ const secretName = this.handleToSecretName.get(handle) ?? handle;
51
+ const url = `${this.daprUrlRoot.replace(/\/+$/, "")}/v1.0/bindings/${this.bindingName}`;
52
+ let resp;
53
+ try {
54
+ resp = await fetch(url, {
55
+ method: "POST",
56
+ headers: { "content-type": "application/json" },
57
+ body: JSON.stringify({
58
+ operation: "get",
59
+ metadata: { path: `/secrets/${secretName}` },
60
+ }),
61
+ });
62
+ }
63
+ catch (e) {
64
+ throw new SecretsError(`env.SECRETS.get(${JSON.stringify(handle)}) failed: ${e.message}`);
65
+ }
66
+ if (!resp.ok) {
67
+ const detail = await safeReadText(resp);
68
+ throw new SecretsError(`env.SECRETS.get(${JSON.stringify(handle)}) failed: HTTP ${resp.status}: ${detail}`);
69
+ }
70
+ let parsed;
71
+ try {
72
+ parsed = await resp.json();
73
+ }
74
+ catch (e) {
75
+ throw new SecretsError(`env.SECRETS.get(${JSON.stringify(handle)}) failed: response is not JSON: ${e.message}`);
76
+ }
77
+ if (typeof parsed !== "object" ||
78
+ parsed === null ||
79
+ !("value" in parsed) ||
80
+ typeof parsed.value !== "string") {
81
+ throw new SecretsError(`env.SECRETS.get(${JSON.stringify(handle)}) failed: response missing 'value' string field`);
82
+ }
83
+ return parsed.value;
84
+ }
85
+ }
86
+ /**
87
+ * Build the `env` object from the runtime-stamped environment variables.
88
+ *
89
+ * Returns an env with:
90
+ * - `env.<telnyx-binding>` — pre-configured Telnyx client, attached
91
+ * under the handle declared by `[telnyx] binding = "..."`.
92
+ * - `env.SECRETS` — `.get(handle)` reads a secret declared in a
93
+ * `[[secrets]]` block.
94
+ *
95
+ * Missing or malformed config yields an empty env rather than throwing,
96
+ * so module load can't crash on a misconfigured deploy. Calls to absent
97
+ * bindings fail at the call site with a standard `undefined`-access error.
98
+ */
99
+ export function buildEnv() {
100
+ const env = {};
101
+ const entries = loadRuntimeBindings();
102
+ const telnyxBaseUrl = (process.env.TELNYX_API_BASE_URL ?? DEFAULT_TELNYX_BASE_URL).replace(/\/+$/, "") +
103
+ TELNYX_API_VERSION_PREFIX;
104
+ const daprPort = process.env.DAPR_HTTP_PORT ?? DEFAULT_DAPR_HTTP_PORT;
105
+ const daprUrlRoot = `http://localhost:${daprPort}`;
106
+ // Materialise env.SECRETS whenever a [[secrets]] entry exists, regardless
107
+ // of whether each carries a secret_name. Entries without one fall through
108
+ // SecretsImpl.get's `?? handle` path, so the customer still gets a
109
+ // working .get() — and if everything's misconfigured, they get the typed
110
+ // SecretsNotConfiguredError instead of a raw TypeError on `env.SECRETS`.
111
+ const secretsHandleMap = new Map();
112
+ for (const e of entries) {
113
+ if (e.type === "secrets" && typeof e.secret_name === "string" && e.secret_name) {
114
+ secretsHandleMap.set(e.binding, e.secret_name);
115
+ }
116
+ }
117
+ if (entries.some((e) => e.type === "secrets")) {
118
+ env.SECRETS = new SecretsImpl(daprUrlRoot, resolveSecretsBindingName(), secretsHandleMap);
119
+ }
120
+ // One Telnyx client is shared across every [telnyx] handle. The loop
121
+ // shape tolerates a future array-of-tables without a code change.
122
+ let telnyxClient = null;
123
+ for (const e of entries) {
124
+ if (e.type !== "telnyx")
125
+ continue;
126
+ if (telnyxClient === null) {
127
+ telnyxClient = new Telnyx({
128
+ apiKey: PLACEHOLDER_API_KEY,
129
+ baseURL: telnyxBaseUrl,
130
+ });
131
+ }
132
+ env[e.binding] = telnyxClient;
133
+ }
134
+ return env;
135
+ }
136
+ // ── helpers ──
137
+ function loadRuntimeBindings() {
138
+ const raw = process.env.TELNYX_RUNTIME_BINDINGS ?? "";
139
+ if (!raw)
140
+ return [];
141
+ let decoded;
142
+ try {
143
+ decoded = JSON.parse(raw);
144
+ }
145
+ catch {
146
+ return [];
147
+ }
148
+ if (!Array.isArray(decoded))
149
+ return [];
150
+ const out = [];
151
+ for (const item of decoded) {
152
+ if (typeof item === "object" &&
153
+ item !== null &&
154
+ typeof item.binding === "string" &&
155
+ item.binding !== "" &&
156
+ typeof item.type === "string") {
157
+ out.push(item);
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ // Explicit TELNYX_SECRETS_BINDING wins; otherwise derive from K_SERVICE;
163
+ // otherwise return "" and env.SECRETS.get() throws SecretsNotConfiguredError.
164
+ function resolveSecretsBindingName() {
165
+ const explicit = process.env.TELNYX_SECRETS_BINDING ?? "";
166
+ if (explicit)
167
+ return explicit;
168
+ const kService = process.env.K_SERVICE ?? "";
169
+ if (kService)
170
+ return `telnyx-secrets-${kService}`;
171
+ return "";
172
+ }
173
+ async function safeReadText(resp) {
174
+ try {
175
+ return (await resp.text()).slice(0, 200);
176
+ }
177
+ catch {
178
+ return "<unreadable>";
179
+ }
180
+ }
181
+ /**
182
+ * The `env` your handler uses. Built lazily on first access:
183
+ *
184
+ * import { env } from "@telnyx/edge-runtime";
185
+ *
186
+ * Lazy so a plain `import` doesn't pay the cost — the work is small but
187
+ * non-zero, and consumers that import the package without touching `env`
188
+ * (e.g. type-only imports, other workspaces) shouldn't trigger it.
189
+ */
190
+ let _env;
191
+ const realEnv = () => (_env ??= buildEnv());
192
+ export const env = new Proxy({}, {
193
+ get: (_, prop) => realEnv()[prop],
194
+ has: (_, prop) => prop in realEnv(),
195
+ ownKeys: () => Reflect.ownKeys(realEnv()),
196
+ getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(realEnv(), prop),
197
+ });
198
+ /** Test seam. Not part of the public API. */
199
+ export const __internal = {
200
+ DEFAULT_TELNYX_BASE_URL,
201
+ TELNYX_API_VERSION_PREFIX,
202
+ DEFAULT_DAPR_HTTP_PORT,
203
+ PLACEHOLDER_API_KEY,
204
+ loadRuntimeBindings,
205
+ resolveSecretsBindingName,
206
+ };
207
+ //# sourceMappingURL=build-env.js.map
package/dist/env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Customer-extensible bindings environment.
3
+ *
4
+ * Customers either pass their own typed env (`StatefulActor<MyEnv>`) or rely on
5
+ * codegen to augment the global `Env` interface.
6
+ */
7
+ export interface Env {
8
+ [binding: string]: unknown;
9
+ }
10
+ //# sourceMappingURL=env.d.ts.map
package/dist/env.js ADDED
@@ -0,0 +1,11 @@
1
+ // Customers extend this via the codegen produced by `telnyx types`. Given:
2
+ //
3
+ // [[actors]]
4
+ // binding = "COUNTER"
5
+ // type = "Counter"
6
+ //
7
+ // `env.COUNTER` becomes typed as a stub against `Counter`'s public method
8
+ // shape. The base type below is intentionally loose so `class MyActor
9
+ // extends StatefulActor<Env>` typechecks before codegen runs.
10
+ export {};
11
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,4 @@
1
+ export type { Env } from "./env.js";
2
+ export { env, buildEnv, SecretsError, SecretsNotConfiguredError } from "./build-env.js";
3
+ export type { Secrets } from "./build-env.js";
4
+ //# sourceMappingURL=public.d.ts.map
package/dist/public.js ADDED
@@ -0,0 +1,11 @@
1
+ // `@telnyx/edge-runtime` — public customer-facing entry.
2
+ //
3
+ // V0 scope: only the `env` surface is published. The rest of the SDK
4
+ // (StatefulActor, ActorContext, ActorStorage, ActorNamespace, the typed
5
+ // actor errors) lives in src/ and is exported from ./index.ts for
6
+ // in-monorepo consumers, but is held back from the npm tarball until the
7
+ // actor path is reachable end-to-end. Re-adding any of those for customers
8
+ // is a single-line re-export here.
9
+ // `env` — the bindings declared in telnyx.toml, ready to use in your handler.
10
+ export { env, buildEnv, SecretsError, SecretsNotConfiguredError } from "./build-env.js";
11
+ //# sourceMappingURL=public.js.map
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@telnyx/edge-runtime",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "description": "Telnyx Edge Compute - Runtime SDK",
6
+ "type": "module",
7
+ "main": "./dist/public.js",
8
+ "types": "./dist/public.d.ts",
9
+ "files": [
10
+ "dist/public.js",
11
+ "dist/public.d.ts",
12
+ "dist/build-env.js",
13
+ "dist/build-env.d.ts",
14
+ "dist/env.js",
15
+ "dist/env.d.ts"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "vitest run",
20
+ "lint": "tsc -p tsconfig.lint.json"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/public.d.ts",
25
+ "import": "./dist/public.js"
26
+ },
27
+ "./internal": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "telnyx": "*",
34
+ "ws": "*"
35
+ }
36
+ }