@miosa/sdk 1.2.2 → 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.2",
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
@@ -136,6 +136,10 @@ export type {
136
136
  DeploymentVersionKind,
137
137
  DeploymentVersionState,
138
138
  DockerDeployCreateParams,
139
+ DockerDeployDoctorCheck,
140
+ DockerDeployDoctorParams,
141
+ DockerDeployDoctorProbe,
142
+ DockerDeployDoctorResult,
139
143
  ExternalAttribution,
140
144
  PublishFromSandboxParams,
141
145
  PublishParams,
@@ -147,6 +151,17 @@ export type {
147
151
  RuntimeLogsResult,
148
152
  VersionListParams,
149
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";
150
165
  export type {
151
166
  SnapshotData,
152
167
  SnapshotStatus,
@@ -3,15 +3,18 @@ import type { HttpClient } from "../http.js";
3
3
  import { Deployments } from "./deployments.js";
4
4
 
5
5
  const mockRequest = vi.fn();
6
+ const mockGet = vi.fn();
6
7
 
7
8
  function makeHttp(): HttpClient {
8
9
  const http = {} as HttpClient;
9
10
  http.request = mockRequest;
11
+ http.get = mockGet;
10
12
  return http;
11
13
  }
12
14
 
13
15
  beforeEach(() => {
14
16
  vi.clearAllMocks();
17
+ vi.unstubAllGlobals();
15
18
  });
16
19
 
17
20
  describe("Deployments", () => {
@@ -52,4 +55,67 @@ describe("Deployments", () => {
52
55
  expect(deployment.deployment_product).toBe("docker_deploy");
53
56
  expect(deployment.docker_deploy_host_id).toBe("ddh_123");
54
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
+ });
55
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
 
@@ -120,6 +121,35 @@ export interface DeploymentData {
120
121
  updated_at?: string;
121
122
  }
122
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
+
123
153
  export type DeploymentDatabaseRequest =
124
154
  | boolean
125
155
  | {
@@ -436,6 +466,43 @@ function dockerDeployMetadata(
436
466
  };
437
467
  }
438
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
+
439
506
  // ── Sub-resources ──────────────────────────────────────────────────────────
440
507
 
441
508
  export class DeploymentVersions {
@@ -656,6 +723,140 @@ export class Deployments {
656
723
  });
657
724
  }
658
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
+
659
860
  async update(
660
861
  deploymentId: string,
661
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
+ });
@@ -0,0 +1,183 @@
1
+ import type { HttpClient } from "../http.js";
2
+
3
+ export type DockerDeployHostId = string & {
4
+ readonly __brand: "DockerDeployHostId";
5
+ };
6
+
7
+ export type DockerDeployHostStatus =
8
+ | "pending"
9
+ | "provisioning"
10
+ | "bootstrapping"
11
+ | "active"
12
+ | "degraded"
13
+ | "suspended"
14
+ | "retired"
15
+ | "error";
16
+
17
+ export type DockerDeployApplianceStatus =
18
+ | "not_installed"
19
+ | "installing"
20
+ | "starting"
21
+ | "healthy"
22
+ | "unhealthy"
23
+ | "unknown";
24
+
25
+ export interface DockerDeployHostData {
26
+ id: DockerDeployHostId;
27
+ tenant_id: string;
28
+ workspace_id: string;
29
+ external_workspace_id?: string | null;
30
+ computer_id?: string | null;
31
+ fleet_node_id?: string | null;
32
+ status: DockerDeployHostStatus;
33
+ size: string;
34
+ region: string;
35
+ portal_domain?: string | null;
36
+ runtime_base_url?: string | null;
37
+ agent_base_url?: string | null;
38
+ appliance_image?: string | null;
39
+ appliance_version?: string | null;
40
+ appliance_status: DockerDeployApplianceStatus;
41
+ agent_last_seen_at?: string | null;
42
+ metadata?: Record<string, unknown>;
43
+ created_at?: string;
44
+ updated_at?: string;
45
+ }
46
+
47
+ export interface DockerDeployHostListParams {
48
+ workspace_id?: string;
49
+ workspaceId?: string;
50
+ }
51
+
52
+ export interface DockerDeployHostEnsureParams {
53
+ workspace_id?: string;
54
+ workspaceId?: string;
55
+ external_workspace_id?: string;
56
+ externalWorkspaceId?: string;
57
+ }
58
+
59
+ export interface DockerDeployHostListResponse {
60
+ data?: DockerDeployHostData[];
61
+ hosts?: DockerDeployHostData[];
62
+ }
63
+
64
+ export interface DockerDeployHostResponse {
65
+ data?: DockerDeployHostData;
66
+ host?: DockerDeployHostData;
67
+ queued?: boolean;
68
+ }
69
+
70
+ export interface DockerDeployTemplate {
71
+ id: string;
72
+ name: string;
73
+ description?: string;
74
+ category?: string;
75
+ runtime?: string;
76
+ tags?: string[];
77
+ metadata?: Record<string, unknown>;
78
+ [key: string]: unknown;
79
+ }
80
+
81
+ export interface DockerDeployTemplateListResponse {
82
+ data?: DockerDeployTemplate[];
83
+ templates?: DockerDeployTemplate[];
84
+ }
85
+
86
+ export interface DockerDeployTemplateResponse {
87
+ data?: DockerDeployTemplate;
88
+ template?: DockerDeployTemplate;
89
+ }
90
+
91
+ function workspaceId(params?: DockerDeployHostListParams): string | undefined {
92
+ return params?.workspace_id ?? params?.workspaceId;
93
+ }
94
+
95
+ function ensureBody(params: DockerDeployHostEnsureParams): Record<string, string> {
96
+ const body: Record<string, string> = {};
97
+ const id = params.workspace_id ?? params.workspaceId;
98
+ const externalId = params.external_workspace_id ?? params.externalWorkspaceId;
99
+ if (id) body.workspace_id = id;
100
+ if (externalId) body.external_workspace_id = externalId;
101
+ return body;
102
+ }
103
+
104
+ function unwrapHost(response: DockerDeployHostResponse): DockerDeployHostData {
105
+ const host = response.data ?? response.host;
106
+ if (!host) {
107
+ throw new Error("Docker Deploy host response was empty.");
108
+ }
109
+ return host;
110
+ }
111
+
112
+ function unwrapTemplates(response: DockerDeployTemplateListResponse): DockerDeployTemplate[] {
113
+ return response.data ?? response.templates ?? [];
114
+ }
115
+
116
+ function unwrapTemplate(response: DockerDeployTemplateResponse): DockerDeployTemplate {
117
+ const template = response.data ?? response.template;
118
+ if (!template) {
119
+ throw new Error("Docker Deploy template response was empty.");
120
+ }
121
+ return template;
122
+ }
123
+
124
+ export class DockerDeploy {
125
+ constructor(private readonly http: HttpClient) {}
126
+
127
+ /**
128
+ * List Docker Deploy appliance hosts scoped to the current tenant.
129
+ *
130
+ * Pass a workspace ID to inspect the dedicated always-on appliance machine
131
+ * for one white-label workspace.
132
+ */
133
+ async listHosts(
134
+ params: DockerDeployHostListParams = {},
135
+ ): Promise<DockerDeployHostData[]> {
136
+ const res = await this.http.get<DockerDeployHostListResponse>(
137
+ "/docker-deploy/hosts",
138
+ { workspace_id: workspaceId(params) },
139
+ );
140
+ return res.data ?? res.hosts ?? [];
141
+ }
142
+
143
+ /**
144
+ * Ensure a workspace has its dedicated Docker Deploy appliance host.
145
+ *
146
+ * The host may still be `pending`, `provisioning`, or `bootstrapping` after
147
+ * this call. Treat `status === "active"` and `appliance_status === "healthy"`
148
+ * as the ready condition before sending app/container traffic to it.
149
+ */
150
+ async ensureHost(
151
+ params: DockerDeployHostEnsureParams = {},
152
+ ): Promise<{ host: DockerDeployHostData; queued: boolean }> {
153
+ const res = await this.http.post<DockerDeployHostResponse>(
154
+ "/docker-deploy/hosts/ensure",
155
+ ensureBody(params),
156
+ );
157
+ return { host: unwrapHost(res), queued: res.queued ?? false };
158
+ }
159
+
160
+ /** Fetch one Docker Deploy host by ID. */
161
+ async getHost(hostId: string): Promise<DockerDeployHostData> {
162
+ const res = await this.http.get<DockerDeployHostResponse>(
163
+ `/docker-deploy/hosts/${hostId}`,
164
+ );
165
+ return unwrapHost(res);
166
+ }
167
+
168
+ /** List Docker Deploy starter templates. */
169
+ async listTemplates(): Promise<DockerDeployTemplate[]> {
170
+ const res = await this.http.get<DockerDeployTemplateListResponse>(
171
+ "/docker-deploy/templates",
172
+ );
173
+ return unwrapTemplates(res);
174
+ }
175
+
176
+ /** Fetch one Docker Deploy starter template by ID. */
177
+ async getTemplate(templateId: string): Promise<DockerDeployTemplate> {
178
+ const res = await this.http.get<DockerDeployTemplateResponse>(
179
+ `/docker-deploy/templates/${encodeURIComponent(templateId)}`,
180
+ );
181
+ return unwrapTemplate(res);
182
+ }
183
+ }