@miosa/sdk 1.2.1 → 1.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miosa/sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "TypeScript SDK for the MIOSA API — cloud VM desktop infrastructure for AI agents",
5
5
  "license": "MIT",
6
6
  "author": "MIOSA <hello@miosa.ai>",
package/src/client.ts CHANGED
@@ -15,6 +15,7 @@ import { CronJobs } from "./resources/cron-jobs.js";
15
15
  import { Dashboard } from "./resources/dashboard.js";
16
16
  import { Databases } from "./resources/databases.js";
17
17
  import { Deployments } from "./resources/deployments.js";
18
+ import { DockerDeploy } from "./resources/docker-deploy.js";
18
19
  import { EgressAudit } from "./resources/egressAudit.js";
19
20
  import { EgressNetwork } from "./resources/egressNetwork.js";
20
21
  import { EgressSecrets } from "./resources/egressSecrets.js";
@@ -130,6 +131,9 @@ export class Miosa {
130
131
  */
131
132
  readonly deployments: Deployments;
132
133
 
134
+ /** Docker Deploy appliance hosts — one always-on workspace host, many apps. */
135
+ readonly dockerDeploy: DockerDeploy;
136
+
133
137
  /** Credit balance and usage. */
134
138
  readonly credits: Credits;
135
139
 
@@ -257,6 +261,7 @@ export class Miosa {
257
261
  this.computers = new Computers(this.http);
258
262
  this.sandboxes = new Sandboxes(this.http);
259
263
  this.deployments = new Deployments(this.http);
264
+ this.dockerDeploy = new DockerDeploy(this.http);
260
265
  this.credits = new Credits(this.http);
261
266
  this.admin = new Admin(this.http);
262
267
  this.openComputers = new OpenComputers(this.http);
package/src/index.ts CHANGED
@@ -122,6 +122,7 @@ export type {
122
122
  DeploymentData,
123
123
  DeploymentId,
124
124
  DeploymentListParams,
125
+ DeploymentProduct,
125
126
  DeploymentReleaseData,
126
127
  DeploymentReleaseId,
127
128
  DeploymentServiceData,
@@ -134,6 +135,11 @@ export type {
134
135
  DeploymentVersionId,
135
136
  DeploymentVersionKind,
136
137
  DeploymentVersionState,
138
+ DockerDeployCreateParams,
139
+ DockerDeployDoctorCheck,
140
+ DockerDeployDoctorParams,
141
+ DockerDeployDoctorProbe,
142
+ DockerDeployDoctorResult,
137
143
  ExternalAttribution,
138
144
  PublishFromSandboxParams,
139
145
  PublishParams,
@@ -145,6 +151,17 @@ export type {
145
151
  RuntimeLogsResult,
146
152
  VersionListParams,
147
153
  } from "./resources/deployments.js";
154
+ export { DockerDeploy } from "./resources/docker-deploy.js";
155
+ export type {
156
+ DockerDeployApplianceStatus,
157
+ DockerDeployHostData,
158
+ DockerDeployHostEnsureParams,
159
+ DockerDeployHostId,
160
+ DockerDeployHostListParams,
161
+ DockerDeployHostListResponse,
162
+ DockerDeployHostResponse,
163
+ DockerDeployHostStatus,
164
+ } from "./resources/docker-deploy.js";
148
165
  export type {
149
166
  SnapshotData,
150
167
  SnapshotStatus,
@@ -216,7 +233,7 @@ export type {
216
233
  HealthCheckCreateParams,
217
234
  HealthCheckUpdateParams,
218
235
  } from "./resources/health-checks.js";
219
- export { Webhooks } from "./resources/webhooks.js";
236
+ export { verifySignature, Webhooks } from "./resources/webhooks.js";
220
237
  export type {
221
238
  WebhookId,
222
239
  WebhookDeliveryId,
@@ -285,7 +302,12 @@ export type {
285
302
 
286
303
  // P2 resources
287
304
  export { Tenant } from "./resources/tenant.js";
288
- export type { TenantPlan } from "./resources/tenant.js";
305
+ export type {
306
+ BrandingData,
307
+ PreviewDomainData,
308
+ TenantBrandingUpdateParams,
309
+ TenantPlan,
310
+ } from "./resources/tenant.js";
289
311
  export { Regions } from "./resources/regions.js";
290
312
  export type {
291
313
  RegionData,
@@ -345,4 +345,15 @@ export class Admin {
345
345
  model_id: modelId,
346
346
  });
347
347
  }
348
+
349
+ /** POST /api/v1/admin/impersonate — returns {token, expires_at}. */
350
+ impersonate(
351
+ externalUserId: string,
352
+ options: { ttlSec?: number } = {},
353
+ ): Promise<{ token: string; expires_at: string }> {
354
+ return this.http.post("/admin/impersonate", {
355
+ external_user_id: externalUserId,
356
+ ttl_sec: options.ttlSec ?? 3600,
357
+ });
358
+ }
348
359
  }
@@ -113,6 +113,22 @@ export class ApiKeys {
113
113
  return unwrap<ApiKeyCreateResult>(data);
114
114
  }
115
115
 
116
+ /** POST /api/v1/api-keys/scoped — L2 delegation token bound to one external user. */
117
+ async createScoped(params: {
118
+ externalUserId: string;
119
+ scopes: string[];
120
+ expiresAt?: string;
121
+ }): Promise<ApiKeyCreateResult> {
122
+ const body = stripUndefined({
123
+ external_user_id: params.externalUserId,
124
+ scopes: params.scopes,
125
+ expires_at: params.expiresAt,
126
+ });
127
+ return unwrap<ApiKeyCreateResult>(
128
+ await this.http.post<unknown>("/api-keys/scoped", body),
129
+ );
130
+ }
131
+
116
132
  async delete(keyId: string): Promise<void> {
117
133
  await this.http.delete<unknown>(`/api-keys/${keyId}`);
118
134
  }
@@ -41,7 +41,7 @@ export interface CustomDomainRegisterParams {
41
41
  * // 1. Register the domain
42
42
  * const domain = await computer.domains.register("app.example.com");
43
43
  * console.log(domain.instructions);
44
- * // => "Add a CNAME record: app.example.com → <slug>.sandbox.<tenant-domain>"
44
+ * // => "Add a CNAME record: app.example.com → <slug>.sandbox.miosa.ai"
45
45
  *
46
46
  * // 2. Add the CNAME in your DNS registrar, then...
47
47
  *
@@ -0,0 +1,121 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { HttpClient } from "../http.js";
3
+ import { Deployments } from "./deployments.js";
4
+
5
+ const mockRequest = vi.fn();
6
+ const mockGet = vi.fn();
7
+
8
+ function makeHttp(): HttpClient {
9
+ const http = {} as HttpClient;
10
+ http.request = mockRequest;
11
+ http.get = mockGet;
12
+ return http;
13
+ }
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ vi.unstubAllGlobals();
18
+ });
19
+
20
+ describe("Deployments", () => {
21
+ it("createDockerDeploy() marks the deployment for Docker Deploy", async () => {
22
+ mockRequest.mockResolvedValue({
23
+ data: {
24
+ id: "dep_123",
25
+ tenant_id: "ten_123",
26
+ name: "Clinic Intake",
27
+ slug: "clinic-intake",
28
+ state: "pending",
29
+ deployment_product: "docker_deploy",
30
+ docker_deploy_host_id: "ddh_123",
31
+ metadata: { deployment_product: "docker_deploy", client: "clinic-iq" },
32
+ },
33
+ });
34
+
35
+ const deployment = await new Deployments(makeHttp()).createDockerDeploy({
36
+ name: "Clinic Intake",
37
+ repoUrl: "https://github.com/clinic-iq/intake",
38
+ externalWorkspaceId: "dr-smith",
39
+ externalProjectId: "lead-magnet",
40
+ metadata: { client: "clinic-iq" },
41
+ idempotencyKey: "idem-123",
42
+ });
43
+
44
+ expect(mockRequest).toHaveBeenCalledWith("/deployments", {
45
+ method: "POST",
46
+ body: {
47
+ name: "Clinic Intake",
48
+ repo_url: "https://github.com/clinic-iq/intake",
49
+ metadata: { client: "clinic-iq", deployment_product: "docker_deploy" },
50
+ external_workspace_id: "dr-smith",
51
+ external_project_id: "lead-magnet",
52
+ },
53
+ headers: { "Idempotency-Key": "idem-123" },
54
+ });
55
+ expect(deployment.deployment_product).toBe("docker_deploy");
56
+ expect(deployment.docker_deploy_host_id).toBe("ddh_123");
57
+ });
58
+
59
+ it("doctorDockerDeploy verifies host, route metadata, and public URL", async () => {
60
+ mockGet.mockImplementation(async (path: string) => {
61
+ if (path === "/deployments/dep_123") {
62
+ return {
63
+ data: {
64
+ id: "dep_123",
65
+ tenant_id: "ten_123",
66
+ name: "Clinic Intake",
67
+ slug: "clinic-intake",
68
+ state: "running",
69
+ deployment_product: "docker_deploy",
70
+ docker_deploy_host_id: "ddh_123",
71
+ public_url: "https://clinic-intake.osa.miosa.app",
72
+ metadata: {
73
+ deployment_product: "docker_deploy",
74
+ runtime: { ip: "172.16.74.246", port: 23906 },
75
+ },
76
+ },
77
+ };
78
+ }
79
+
80
+ if (path === "/docker-deploy/hosts/ddh_123") {
81
+ return {
82
+ data: {
83
+ id: "ddh_123",
84
+ tenant_id: "ten_123",
85
+ workspace_id: "ws_123",
86
+ status: "active",
87
+ size: "medium",
88
+ region: "us",
89
+ appliance_status: "healthy",
90
+ },
91
+ };
92
+ }
93
+
94
+ throw new Error(`unexpected path ${path}`);
95
+ });
96
+ vi.stubGlobal(
97
+ "fetch",
98
+ vi.fn().mockResolvedValue({ ok: true, status: 200 }),
99
+ );
100
+
101
+ const result = await new Deployments(makeHttp()).doctorDockerDeploy(
102
+ "dep_123",
103
+ { probePath: "/health" },
104
+ );
105
+
106
+ expect(result.ok).toBe(true);
107
+ expect(mockGet).toHaveBeenCalledWith("/deployments/dep_123");
108
+ expect(mockGet).toHaveBeenCalledWith("/docker-deploy/hosts/ddh_123");
109
+ expect(fetch).toHaveBeenCalledWith(
110
+ "https://clinic-intake.osa.miosa.app/health",
111
+ expect.objectContaining({ method: "GET" }),
112
+ );
113
+ expect(result.checks.map((check) => check.name)).toEqual([
114
+ "deployment_product",
115
+ "docker_deploy_host_id",
116
+ "docker_deploy_host_health",
117
+ "runtime_route",
118
+ "public_url_probe",
119
+ ]);
120
+ });
121
+ });
@@ -13,6 +13,7 @@
13
13
  import { randomUUID } from "node:crypto";
14
14
 
15
15
  import type { HttpClient } from "../http.js";
16
+ import type { DockerDeployHostData } from "./docker-deploy.js";
16
17
 
17
18
  // ── Branded IDs ─────────────────────────────────────────────────────────────
18
19
 
@@ -50,6 +51,8 @@ export type DeploymentVersionState =
50
51
 
51
52
  export type DeploymentSourceType = "repo" | "sandbox" | "upload";
52
53
 
54
+ export type DeploymentProduct = "miosa_deploy" | "docker_deploy";
55
+
53
56
  export type DeploymentServiceType =
54
57
  | "static_web"
55
58
  | "web"
@@ -107,6 +110,8 @@ export interface DeploymentData {
107
110
  auto_deploy?: boolean;
108
111
  custom_domain_id?: string | null;
109
112
  linked_database_id?: string | null;
113
+ deployment_product?: DeploymentProduct | string | null;
114
+ docker_deploy_host_id?: string | null;
110
115
  metadata?: Record<string, unknown>;
111
116
  external_workspace_id?: string | null;
112
117
  external_user_id?: string | null;
@@ -116,10 +121,39 @@ export interface DeploymentData {
116
121
  updated_at?: string;
117
122
  }
118
123
 
124
+ export interface DockerDeployDoctorCheck {
125
+ name: string;
126
+ ok: boolean;
127
+ message: string;
128
+ details?: Record<string, unknown>;
129
+ }
130
+
131
+ export interface DockerDeployDoctorProbe {
132
+ url: string;
133
+ ok: boolean;
134
+ status?: number;
135
+ error?: string;
136
+ }
137
+
138
+ export interface DockerDeployDoctorResult {
139
+ ok: boolean;
140
+ deployment: DeploymentData;
141
+ host?: DockerDeployHostData;
142
+ checks: DockerDeployDoctorCheck[];
143
+ probe?: DockerDeployDoctorProbe;
144
+ }
145
+
146
+ export interface DockerDeployDoctorParams {
147
+ probePath?: string;
148
+ probe_path?: string;
149
+ timeoutMs?: number;
150
+ timeout_ms?: number;
151
+ }
152
+
119
153
  export type DeploymentDatabaseRequest =
120
154
  | boolean
121
155
  | {
122
- engine?: "postgresql" | "mysql" | "redis";
156
+ engine?: "postgresql" | "mysql" | "redis" | "qdrant";
123
157
  size?: "xs" | "small" | "medium" | "large";
124
158
  storage_mb?: number;
125
159
  region?: string;
@@ -281,6 +315,8 @@ export interface DeploymentCreateParams extends ExternalAttribution {
281
315
  idempotencyKey?: string;
282
316
  }
283
317
 
318
+ export interface DockerDeployCreateParams extends DeploymentCreateParams {}
319
+
284
320
  export interface DeploymentUpdateParams {
285
321
  name?: string;
286
322
  branch?: string;
@@ -421,6 +457,52 @@ function stripUndefined(
421
457
  );
422
458
  }
423
459
 
460
+ function dockerDeployMetadata(
461
+ metadata: Record<string, unknown> | undefined,
462
+ ): Record<string, unknown> {
463
+ return {
464
+ ...(metadata ?? {}),
465
+ deployment_product: "docker_deploy",
466
+ };
467
+ }
468
+
469
+ function dockerDeployProduct(deployment: DeploymentData): unknown {
470
+ return (
471
+ deployment.deployment_product ??
472
+ deployment.metadata?.["deployment_product"]
473
+ );
474
+ }
475
+
476
+ function dockerDeployHostId(deployment: DeploymentData): string | null {
477
+ const metadataHost = deployment.metadata?.["docker_deploy_host_id"];
478
+ return (
479
+ deployment.docker_deploy_host_id ??
480
+ (typeof metadataHost === "string" ? metadataHost : null)
481
+ );
482
+ }
483
+
484
+ function addDoctorCheck(
485
+ checks: DockerDeployDoctorCheck[],
486
+ name: string,
487
+ ok: boolean,
488
+ message: string,
489
+ details?: Record<string, unknown>,
490
+ ): void {
491
+ checks.push({ name, ok, message, ...(details ? { details } : {}) });
492
+ }
493
+
494
+ function hostHealthy(host: DockerDeployHostData | undefined): boolean {
495
+ return Boolean(
496
+ host && host.status === "active" && host.appliance_status === "healthy",
497
+ );
498
+ }
499
+
500
+ function probeUrl(publicUrl: string, probePath: string): string {
501
+ const url = new URL(publicUrl);
502
+ url.pathname = probePath.startsWith("/") ? probePath : `/${probePath}`;
503
+ return url.toString();
504
+ }
505
+
424
506
  // ── Sub-resources ──────────────────────────────────────────────────────────
425
507
 
426
508
  export class DeploymentVersions {
@@ -627,6 +709,154 @@ export class Deployments {
627
709
  return unwrap(data) as DeploymentData;
628
710
  }
629
711
 
712
+ /**
713
+ * Create a deployment that runs on the workspace's dedicated Docker Deploy
714
+ * runtime. It uses the same /deployments API as MIOSA Deploy, but marks the
715
+ * deployment so the control plane attaches it to the workspace Docker host.
716
+ */
717
+ async createDockerDeploy(
718
+ params: DockerDeployCreateParams,
719
+ ): Promise<DeploymentData> {
720
+ return this.create({
721
+ ...params,
722
+ metadata: dockerDeployMetadata(params.metadata),
723
+ });
724
+ }
725
+
726
+ /**
727
+ * Verify a Docker Deploy deployment before telling a user or agent it is
728
+ * live. Checks product markers, appliance host health, route metadata, and
729
+ * optionally probes the public URL.
730
+ */
731
+ async doctorDockerDeploy(
732
+ deploymentId: string,
733
+ params: DockerDeployDoctorParams = {},
734
+ ): Promise<DockerDeployDoctorResult> {
735
+ const checks: DockerDeployDoctorCheck[] = [];
736
+ const deployment = await this.get(deploymentId);
737
+ const metadata = deployment.metadata ?? {};
738
+ const product = dockerDeployProduct(deployment);
739
+ const hostId = dockerDeployHostId(deployment);
740
+
741
+ addDoctorCheck(
742
+ checks,
743
+ "deployment_product",
744
+ product === "docker_deploy",
745
+ product === "docker_deploy"
746
+ ? "Deployment is marked for Docker Deploy."
747
+ : `Expected deployment_product=docker_deploy, got ${String(product ?? "missing")}.`,
748
+ { deployment_product: product ?? null },
749
+ );
750
+
751
+ addDoctorCheck(
752
+ checks,
753
+ "docker_deploy_host_id",
754
+ Boolean(hostId),
755
+ hostId
756
+ ? "Deployment has a Docker Deploy host id."
757
+ : "Deployment has no docker_deploy_host_id.",
758
+ { docker_deploy_host_id: hostId },
759
+ );
760
+
761
+ let host: DockerDeployHostData | undefined;
762
+ if (hostId) {
763
+ try {
764
+ const rawHost = await this.http.get<unknown>(
765
+ `/docker-deploy/hosts/${hostId}`,
766
+ );
767
+ host = unwrap(rawHost) as DockerDeployHostData;
768
+ addDoctorCheck(
769
+ checks,
770
+ "docker_deploy_host_health",
771
+ hostHealthy(host),
772
+ hostHealthy(host)
773
+ ? "Docker Deploy host is active and healthy."
774
+ : `Docker Deploy host status=${host.status} appliance=${host.appliance_status}.`,
775
+ {
776
+ status: host.status,
777
+ appliance_status: host.appliance_status,
778
+ },
779
+ );
780
+ } catch (error) {
781
+ addDoctorCheck(
782
+ checks,
783
+ "docker_deploy_host_health",
784
+ false,
785
+ error instanceof Error ? error.message : String(error),
786
+ );
787
+ }
788
+ }
789
+
790
+ const runtime = metadata["runtime"];
791
+ const hasRuntimeRoute =
792
+ typeof runtime === "object" &&
793
+ runtime !== null &&
794
+ typeof (runtime as Record<string, unknown>)["ip"] === "string" &&
795
+ typeof (runtime as Record<string, unknown>)["port"] === "number";
796
+ addDoctorCheck(
797
+ checks,
798
+ "runtime_route",
799
+ hasRuntimeRoute,
800
+ hasRuntimeRoute
801
+ ? "Deployment has appliance runtime route metadata."
802
+ : "Deployment is missing appliance runtime route metadata.",
803
+ typeof runtime === "object" && runtime !== null
804
+ ? (runtime as Record<string, unknown>)
805
+ : undefined,
806
+ );
807
+
808
+ let probe: DockerDeployDoctorProbe | undefined;
809
+ const publicUrl = deployment.public_url;
810
+ const path = params.probePath ?? params.probe_path ?? "/";
811
+ if (publicUrl && typeof fetch === "function") {
812
+ const url = probeUrl(publicUrl, path);
813
+ const controller = new AbortController();
814
+ const timeout = setTimeout(
815
+ () => controller.abort(),
816
+ params.timeoutMs ?? params.timeout_ms ?? 10_000,
817
+ );
818
+ try {
819
+ const response = await fetch(url, {
820
+ method: "GET",
821
+ signal: controller.signal,
822
+ });
823
+ probe = { url, ok: response.ok, status: response.status };
824
+ addDoctorCheck(
825
+ checks,
826
+ "public_url_probe",
827
+ response.ok,
828
+ response.ok
829
+ ? `Public URL returned HTTP ${response.status}.`
830
+ : `Public URL returned HTTP ${response.status}.`,
831
+ { url, status: response.status },
832
+ );
833
+ } catch (error) {
834
+ probe = {
835
+ url,
836
+ ok: false,
837
+ error: error instanceof Error ? error.message : String(error),
838
+ };
839
+ addDoctorCheck(
840
+ checks,
841
+ "public_url_probe",
842
+ false,
843
+ probe.error ?? "Public URL probe failed.",
844
+ { url },
845
+ );
846
+ } finally {
847
+ clearTimeout(timeout);
848
+ }
849
+ }
850
+
851
+ return {
852
+ ok: checks.every((check) => check.ok),
853
+ deployment,
854
+ ...(host ? { host } : {}),
855
+ checks,
856
+ ...(probe ? { probe } : {}),
857
+ };
858
+ }
859
+
630
860
  async update(
631
861
  deploymentId: string,
632
862
  params: DeploymentUpdateParams,
@@ -0,0 +1,102 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { HttpClient } from "../http.js";
3
+ import { DockerDeploy } from "./docker-deploy.js";
4
+
5
+ const mockGet = vi.fn();
6
+ const mockPost = vi.fn();
7
+
8
+ function makeHttp(): HttpClient {
9
+ const http = {} as HttpClient;
10
+ http.get = mockGet;
11
+ http.post = mockPost;
12
+ return http;
13
+ }
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ describe("DockerDeploy", () => {
20
+ it("lists Docker Deploy hosts by workspace", async () => {
21
+ mockGet.mockResolvedValue({
22
+ data: [
23
+ {
24
+ id: "ddh_123",
25
+ tenant_id: "ten_123",
26
+ workspace_id: "ws_123",
27
+ status: "bootstrapping",
28
+ appliance_status: "starting",
29
+ size: "medium",
30
+ region: "us",
31
+ },
32
+ ],
33
+ });
34
+
35
+ const hosts = await new DockerDeploy(makeHttp()).listHosts({
36
+ workspaceId: "ws_123",
37
+ });
38
+
39
+ expect(mockGet).toHaveBeenCalledWith("/docker-deploy/hosts", {
40
+ workspace_id: "ws_123",
41
+ });
42
+ expect(hosts[0]?.id).toBe("ddh_123");
43
+ });
44
+
45
+ it("ensures a workspace Docker Deploy host", async () => {
46
+ mockPost.mockResolvedValue({
47
+ host: {
48
+ id: "ddh_123",
49
+ tenant_id: "ten_123",
50
+ workspace_id: "ws_123",
51
+ status: "pending",
52
+ appliance_status: "not_installed",
53
+ size: "medium",
54
+ region: "us",
55
+ },
56
+ queued: true,
57
+ });
58
+
59
+ const result = await new DockerDeploy(makeHttp()).ensureHost({
60
+ workspaceId: "ws_123",
61
+ });
62
+
63
+ expect(mockPost).toHaveBeenCalledWith("/docker-deploy/hosts/ensure", {
64
+ workspace_id: "ws_123",
65
+ });
66
+ expect(result.queued).toBe(true);
67
+ expect(result.host.status).toBe("pending");
68
+ });
69
+
70
+ it("lists Docker Deploy templates", async () => {
71
+ mockGet.mockResolvedValue({
72
+ data: [
73
+ {
74
+ id: "nextjs-app",
75
+ name: "Next.js app",
76
+ description: "App router starter",
77
+ },
78
+ ],
79
+ });
80
+
81
+ const templates = await new DockerDeploy(makeHttp()).listTemplates();
82
+
83
+ expect(mockGet).toHaveBeenCalledWith("/docker-deploy/templates");
84
+ expect(templates[0]?.id).toBe("nextjs-app");
85
+ });
86
+
87
+ it("gets a Docker Deploy template", async () => {
88
+ mockGet.mockResolvedValue({
89
+ template: {
90
+ id: "compose-full-stack",
91
+ name: "Compose full stack",
92
+ },
93
+ });
94
+
95
+ const template = await new DockerDeploy(makeHttp()).getTemplate("compose-full-stack");
96
+
97
+ expect(mockGet).toHaveBeenCalledWith(
98
+ "/docker-deploy/templates/compose-full-stack",
99
+ );
100
+ expect(template.id).toBe("compose-full-stack");
101
+ });
102
+ });