ds-01 1.0.4 → 1.0.6

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.
@@ -0,0 +1,266 @@
1
+ // cli/src/utils/secureKeyStore.ts
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ const execFileAsync = promisify(execFile);
8
+ const KEY_NAME = "DS01-CLI-Device-Key";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ function getMacHelperPath() {
12
+ // dist/utils/secureKeyStore.js
13
+ // ../native/macos-key-helper.swift
14
+ return path.resolve(__dirname, "../native/macos-key-helper.swift");
15
+ }
16
+ function ensureSupportedOS() {
17
+ const platform = os.platform();
18
+ if (platform !== "win32" && platform !== "darwin") {
19
+ throw new Error("DS01 secure device keys currently support Windows and macOS.");
20
+ }
21
+ }
22
+ /**
23
+ * ---------------------------------------------------------
24
+ * WINDOWS
25
+ * ---------------------------------------------------------
26
+ */
27
+ async function createWindowsDeviceKey() {
28
+ const script = `
29
+ $ErrorActionPreference = "Stop"
30
+
31
+ $existing = Get-ChildItem Cert:\\CurrentUser\\My |
32
+ Where-Object {
33
+ $_.Subject -eq "CN=${KEY_NAME}"
34
+ } |
35
+ Select-Object -First 1
36
+
37
+ if ($existing) {
38
+ Write-Output "EXISTS"
39
+ exit 0
40
+ }
41
+
42
+ $params = @{
43
+ Type = "Custom"
44
+ Subject = "CN=${KEY_NAME}"
45
+
46
+ Provider = "Microsoft Platform Crypto Provider"
47
+
48
+ KeyAlgorithm = "RSA"
49
+ KeyLength = 2048
50
+
51
+ KeyExportPolicy = "NonExportable"
52
+
53
+ KeyUsage = "DigitalSignature"
54
+ KeySpec = "Signature"
55
+
56
+ CertStoreLocation = "Cert:\\CurrentUser\\My"
57
+
58
+ NotAfter = (Get-Date).AddYears(10)
59
+ }
60
+
61
+ New-SelfSignedCertificate @params | Out-Null
62
+
63
+ Write-Output "CREATED"
64
+ `;
65
+ const { stdout } = await execFileAsync("powershell.exe", [
66
+ "-NoProfile",
67
+ "-NonInteractive",
68
+ "-ExecutionPolicy",
69
+ "Bypass",
70
+ "-Command",
71
+ script,
72
+ ], {
73
+ windowsHide: true,
74
+ });
75
+ const result = stdout.trim();
76
+ if (result !== "CREATED" && result !== "EXISTS") {
77
+ throw new Error("Failed to create Windows DS01 device key.");
78
+ }
79
+ }
80
+ async function getWindowsPublicKey() {
81
+ const script = `
82
+ $ErrorActionPreference = "Stop"
83
+
84
+ $cert = Get-ChildItem Cert:\\CurrentUser\\My |
85
+ Where-Object {
86
+ $_.Subject -eq "CN=${KEY_NAME}"
87
+ } |
88
+ Select-Object -First 1
89
+
90
+ if (-not $cert) {
91
+ throw "DS01 device key not found."
92
+ }
93
+
94
+ [Convert]::ToBase64String($cert.RawData)
95
+ `;
96
+ const { stdout } = await execFileAsync("powershell.exe", [
97
+ "-NoProfile",
98
+ "-NonInteractive",
99
+ "-ExecutionPolicy",
100
+ "Bypass",
101
+ "-Command",
102
+ script,
103
+ ], {
104
+ windowsHide: true,
105
+ });
106
+ const certificate = stdout.trim();
107
+ if (!certificate) {
108
+ throw new Error("Unable to read Windows DS01 public certificate.");
109
+ }
110
+ return certificate;
111
+ }
112
+ async function signWindowsDeviceKey(data) {
113
+ const dataBase64 = Buffer.from(data).toString("base64");
114
+ const script = `
115
+ $ErrorActionPreference = "Stop"
116
+
117
+ $cert = Get-ChildItem Cert:\\CurrentUser\\My |
118
+ Where-Object {
119
+ $_.Subject -eq "CN=${KEY_NAME}"
120
+ } |
121
+ Select-Object -First 1
122
+
123
+ if (-not $cert) {
124
+ throw "DS01 device key not found."
125
+ }
126
+
127
+ if (-not $cert.HasPrivateKey) {
128
+ throw "DS01 device certificate has no private key."
129
+ }
130
+
131
+ $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
132
+
133
+ if (-not $rsa) {
134
+ throw "Unable to access DS01 private signing key."
135
+ }
136
+
137
+ try {
138
+ $data = [Convert]::FromBase64String("${dataBase64}")
139
+
140
+ $signature = $rsa.SignData(
141
+ $data,
142
+ [System.Security.Cryptography.HashAlgorithmName]::SHA256,
143
+ [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
144
+ )
145
+
146
+ [Convert]::ToBase64String($signature)
147
+ }
148
+ finally {
149
+ $rsa.Dispose()
150
+ }
151
+ `;
152
+ const { stdout } = await execFileAsync("powershell.exe", [
153
+ "-NoProfile",
154
+ "-NonInteractive",
155
+ "-ExecutionPolicy",
156
+ "Bypass",
157
+ "-Command",
158
+ script,
159
+ ], {
160
+ windowsHide: true,
161
+ });
162
+ const signature = stdout.trim();
163
+ if (!signature) {
164
+ throw new Error("Failed to sign data with Windows device key.");
165
+ }
166
+ return new Uint8Array(Buffer.from(signature, "base64"));
167
+ }
168
+ async function deleteWindowsDeviceKey() {
169
+ const script = `
170
+ $ErrorActionPreference = "Stop"
171
+
172
+ $cert = Get-ChildItem Cert:\\CurrentUser\\My |
173
+ Where-Object {
174
+ $_.Subject -eq "CN=${KEY_NAME}"
175
+ } |
176
+ Select-Object -First 1
177
+
178
+ if ($cert) {
179
+ Remove-Item $cert.PSPath
180
+ }
181
+ `;
182
+ await execFileAsync("powershell.exe", [
183
+ "-NoProfile",
184
+ "-NonInteractive",
185
+ "-ExecutionPolicy",
186
+ "Bypass",
187
+ "-Command",
188
+ script,
189
+ ], {
190
+ windowsHide: true,
191
+ });
192
+ }
193
+ /**
194
+ * ---------------------------------------------------------
195
+ * MACOS
196
+ * ---------------------------------------------------------
197
+ */
198
+ async function runMacHelper(operation, data) {
199
+ const helper = getMacHelperPath();
200
+ const args = [helper, operation];
201
+ if (data) {
202
+ args.push(Buffer.from(data).toString("base64"));
203
+ }
204
+ const { stdout } = await execFileAsync("swift", args, {
205
+ windowsHide: true,
206
+ });
207
+ return stdout.trim();
208
+ }
209
+ async function createMacDeviceKey() {
210
+ const result = await runMacHelper("create");
211
+ if (result !== "CREATED" && result !== "EXISTS") {
212
+ throw new Error(result || "Failed to create macOS Secure Enclave device key.");
213
+ }
214
+ }
215
+ async function getMacPublicKey() {
216
+ const result = await runMacHelper("public");
217
+ if (!result) {
218
+ throw new Error("Unable to read macOS DS01 public key.");
219
+ }
220
+ return result;
221
+ }
222
+ async function signMacDeviceKey(data) {
223
+ const result = await runMacHelper("sign", data);
224
+ if (!result) {
225
+ throw new Error("Failed to sign data with macOS Secure Enclave key.");
226
+ }
227
+ return new Uint8Array(Buffer.from(result, "base64"));
228
+ }
229
+ async function deleteMacDeviceKey() {
230
+ await runMacHelper("delete");
231
+ }
232
+ /**
233
+ * ---------------------------------------------------------
234
+ * PUBLIC API
235
+ * ---------------------------------------------------------
236
+ */
237
+ export async function createDeviceKey() {
238
+ ensureSupportedOS();
239
+ if (os.platform() === "win32") {
240
+ await createWindowsDeviceKey();
241
+ return;
242
+ }
243
+ await createMacDeviceKey();
244
+ }
245
+ export async function getDevicePublicKey() {
246
+ ensureSupportedOS();
247
+ if (os.platform() === "win32") {
248
+ return getWindowsPublicKey();
249
+ }
250
+ return getMacPublicKey();
251
+ }
252
+ export async function signWithDeviceKey(data) {
253
+ ensureSupportedOS();
254
+ if (os.platform() === "win32") {
255
+ return signWindowsDeviceKey(data);
256
+ }
257
+ return signMacDeviceKey(data);
258
+ }
259
+ export async function deleteDeviceKey() {
260
+ ensureSupportedOS();
261
+ if (os.platform() === "win32") {
262
+ await deleteWindowsDeviceKey();
263
+ return;
264
+ }
265
+ await deleteMacDeviceKey();
266
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ds-01",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Make your site best with DS01",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "@clack/prompts": "^1.7.0",
17
17
  "chalk": "^5.6.2",
18
18
  "commander": "^14",
19
+ "cross-keychain": "^1.1.0",
19
20
  "node-machine-id": "^1",
20
21
  "open": "^10",
21
22
  "ora": "^8",
@@ -4,46 +4,60 @@ import pc from "picocolors";
4
4
  import fs from "fs";
5
5
  import path from "path";
6
6
  import { execSync } from "child_process";
7
- import { getToken, getMachineId } from "../utils/config.js";
8
7
 
9
- const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
8
+ import { getToken } from "../utils/config.js";
9
+ import { signWithDeviceKey } from "../utils/secureKeyStore.js";
10
+
11
+ import machineIdPkg from "node-machine-id";
12
+
13
+ const { machineIdSync } = machineIdPkg;
14
+
15
+ const API_BASE_URL =
16
+ process.env.DS01_API_URL || "https://ds-01.vercel.app";
10
17
 
11
18
  // Helper to verify Tailwind exists before doing anything
12
19
  function checkTailwindInstallation() {
13
20
  const targetDir = process.cwd();
14
21
  const pkgJsonPath = path.join(targetDir, "package.json");
15
22
 
16
- // 1. Ensure they are in a valid Node.js project
17
23
  if (!fs.existsSync(pkgJsonPath)) {
18
24
  cancel(
19
25
  pc.red(
20
- "No package.json found. Please run this command inside a Node.js project."
21
- )
26
+ "No package.json found. Please run this command inside a Node.js project.",
27
+ ),
22
28
  );
23
29
  process.exit(1);
24
30
  }
25
31
 
26
- // 2. Read package.json dependencies
27
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
32
+ const pkgJson = JSON.parse(
33
+ fs.readFileSync(pkgJsonPath, "utf-8"),
34
+ );
35
+
28
36
  const allDeps = {
29
37
  ...pkgJson.dependencies,
30
38
  ...pkgJson.devDependencies,
31
39
  };
32
40
 
33
- // 3. Strict Tailwind Check
34
41
  if (!allDeps["tailwindcss"]) {
35
42
  cancel(
36
- pc.red("Tailwind CSS is missing from your project dependencies.\n") +
43
+ pc.red(
44
+ "Tailwind CSS is missing from your project dependencies.\n",
45
+ ) +
37
46
  pc.gray(
38
- "DS01 components rely strictly on Tailwind CSS for styling.\n\n"
47
+ "DS01 components rely strictly on Tailwind CSS for styling.\n\n",
39
48
  ) +
40
49
  pc.white(
41
- "Kindly install it and configure your project, then retry:\n"
50
+ "Kindly install it and configure your project, then retry:\n",
51
+ ) +
52
+ pc.cyan(
53
+ " npm install tailwindcss @tailwindcss/postcss postcss\n\n",
42
54
  ) +
43
- pc.cyan(" npm install tailwindcss @tailwindcss/postcss postcss\n\n") +
44
55
  pc.gray("Official Setup Guide: ") +
45
- pc.underline("https://tailwindcss.com/docs/installation")
56
+ pc.underline(
57
+ "https://tailwindcss.com/docs/installation",
58
+ ),
46
59
  );
60
+
47
61
  process.exit(1);
48
62
  }
49
63
  }
@@ -51,134 +65,275 @@ function checkTailwindInstallation() {
51
65
  export const add = new Command()
52
66
  .name("add")
53
67
  .description("Add a component from DS01 to your project")
54
- .argument("<component>", "The name of the component (e.g., premium-section)")
68
+ .argument(
69
+ "<component>",
70
+ "The name of the component (e.g., premium-section)",
71
+ )
55
72
  .action(async (componentName: string) => {
56
- console.log(); // Spacing for visual breathing room
73
+ console.log();
57
74
 
58
- // 1. Premium Header
59
75
  intro(
60
- `${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray("Adding Component")}`
76
+ `${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray(
77
+ "Adding Component",
78
+ )}`,
61
79
  );
62
80
 
63
- // 2. Run Pre-Flight Tailwind Check
64
81
  checkTailwindInstallation();
65
82
 
66
83
  const cwd = process.cwd();
67
- const configPath = path.join(cwd, "ds01.config.json");
84
+
85
+ const configPath = path.join(
86
+ cwd,
87
+ "ds01.config.json",
88
+ );
68
89
 
69
90
  if (!fs.existsSync(configPath)) {
70
- cancel(pc.red("ds01.config.json not found. Run 'npx ds-01 init' first."));
91
+ cancel(
92
+ pc.red(
93
+ "ds01.config.json not found. Run 'npx ds-01 init' first.",
94
+ ),
95
+ );
96
+
71
97
  process.exit(1);
72
98
  }
73
99
 
74
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
75
-
76
- // Read the GLOBAL credentials, not the local project config
100
+ const config = JSON.parse(
101
+ fs.readFileSync(configPath, "utf-8"),
102
+ );
103
+
77
104
  const token = getToken();
78
- const machineId = getMachineId();
105
+ const machineId = machineIdSync();
79
106
 
80
107
  if (!token || !machineId) {
81
- cancel(pc.red("You are not authenticated. Run 'npx ds-01 login' first."));
108
+ cancel(
109
+ pc.red(
110
+ "You are not authenticated. Run 'npx ds-01 login' first.",
111
+ ),
112
+ );
113
+
82
114
  process.exit(1);
83
115
  }
84
116
 
85
117
  const s = spinner();
86
- s.start(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
118
+
119
+ s.start(
120
+ `Fetching ${pc.cyan(
121
+ `<${componentName} />`,
122
+ )} from registry...`,
123
+ );
87
124
 
88
125
  try {
89
- // 3. Fetch component from your API
126
+ // --------------------------------------------------
127
+ // 1. Create request-specific message
128
+ // --------------------------------------------------
129
+
130
+ const timestamp = Date.now().toString();
131
+
132
+ const message = [
133
+ "GET",
134
+ `/api/registry/${componentName}`,
135
+ timestamp,
136
+ machineId,
137
+ ].join("\n");
138
+
139
+ // --------------------------------------------------
140
+ // 2. Sign message using the device private key
141
+ // --------------------------------------------------
142
+
143
+ const signature = await signWithDeviceKey(
144
+ new TextEncoder().encode(message),
145
+ );
146
+
147
+ const signatureBase64 =
148
+ Buffer.from(signature).toString("base64");
149
+
150
+ // --------------------------------------------------
151
+ // 3. Send token + machine ID + signature
152
+ // --------------------------------------------------
153
+
90
154
  const response = await fetch(
91
155
  `${API_BASE_URL}/api/registry/${componentName}`,
92
156
  {
157
+ method: "GET",
158
+
93
159
  headers: {
94
160
  Authorization: `Bearer ${token}`,
95
- "X-Machine-ID": machineId as string, // <-- TypeScript error fixed here
161
+
162
+ "X-Machine-ID": machineId,
163
+
164
+ "X-DS01-Timestamp": timestamp,
165
+
166
+ "X-DS01-Signature": signatureBase64,
96
167
  },
97
- }
168
+ },
98
169
  );
99
170
 
100
171
  if (!response.ok) {
101
- s.stop(pc.red("Component fetch failed."));
102
- cancel(`Component not found or server error (HTTP ${response.status})`);
172
+ s.stop(
173
+ pc.red("Component fetch failed."),
174
+ );
175
+
176
+ cancel(
177
+ `Component not found or server error (HTTP ${response.status})`,
178
+ );
179
+
103
180
  process.exit(1);
104
181
  }
105
182
 
106
- const componentData = await response.json();
183
+ const componentData =
184
+ await response.json();
185
+
186
+ // --------------------------------------------------
187
+ // 4. Create component folder
188
+ // --------------------------------------------------
189
+
190
+ const targetDir = path.join(
191
+ cwd,
192
+ config.componentsPath,
193
+ componentName,
194
+ );
107
195
 
108
- // 4. Create dedicated component folder
109
- const targetDir = path.join(cwd, config.componentsPath, componentName);
110
196
  if (!fs.existsSync(targetDir)) {
111
- fs.mkdirSync(targetDir, { recursive: true });
197
+ fs.mkdirSync(targetDir, {
198
+ recursive: true,
199
+ });
112
200
  }
113
201
 
114
- // 🛠️ EXTRACT NAMES FOR SMART REWRITING (e.g., ["PremiumHero", "Section", "AnimatedText"])
115
- // We strip the extension (.tsx, .ts) to get the raw component name for import matching
116
- const bundledFileNames = componentData.files.map((file: any) =>
117
- file.name.replace(/\.[^/.]+$/, "")
118
- );
202
+ // --------------------------------------------------
203
+ // 5. Extract bundled file names
204
+ // --------------------------------------------------
205
+
206
+ const bundledFileNames =
207
+ componentData.files.map(
208
+ (file: any) =>
209
+ file.name.replace(
210
+ /\.[^/.]+$/,
211
+ "",
212
+ ),
213
+ );
214
+
215
+ // --------------------------------------------------
216
+ // 6. Write component files
217
+ // --------------------------------------------------
119
218
 
120
- // 5. Inject the pure code files
121
219
  for (const file of componentData.files) {
122
- // Because the server sends just the basename (e.g., PremiumHero.tsx),
123
- // path.join inherently flattens all files into the single targetDir folder.
124
- const filePath = path.join(targetDir, file.name);
220
+ const filePath = path.join(
221
+ targetDir,
222
+ file.name,
223
+ );
224
+
125
225
  let content = file.content;
126
226
 
127
- // 🛠️ DYNAMIC IMPORT REWRITER
128
- bundledFileNames.forEach((fileName: string) => {
129
- // Finds any relative import pointing to a bundled file (e.g., "../Section", "../../Section")
130
- // and rewrites it strictly to a sibling import (e.g., "./Section")
131
- const regex = new RegExp(`from\\s+["']\\.[^"']*?\\/${fileName}["']`, "g");
132
- content = content.replace(regex, `from "./${fileName}"`);
133
- });
227
+ bundledFileNames.forEach(
228
+ (fileName: string) => {
229
+ const regex = new RegExp(
230
+ `from\\s+["']\\.[^"']*?\\/${fileName}["']`,
231
+ "g",
232
+ );
233
+
234
+ content = content.replace(
235
+ regex,
236
+ `from "./${fileName}"`,
237
+ );
238
+ },
239
+ );
134
240
 
135
- fs.writeFileSync(filePath, content);
241
+ fs.writeFileSync(
242
+ filePath,
243
+ content,
244
+ );
136
245
  }
137
246
 
138
247
  s.stop(
139
248
  pc.green(
140
- `Downloaded ${componentData.files.length} files into ${pc.white(`/${config.componentsPath}/${componentName}`)}`
141
- )
249
+ `Downloaded ${componentData.files.length} files into ${pc.white(
250
+ `/${config.componentsPath}/${componentName}`,
251
+ )}`,
252
+ ),
142
253
  );
143
254
 
144
- // 6. Auto-install Missing Component Dependencies (e.g., framer-motion)
145
- if (componentData.dependencies && componentData.dependencies.length > 0) {
146
- const depsToInstall = componentData.dependencies.join(" ");
255
+ // --------------------------------------------------
256
+ // 7. Install dependencies
257
+ // --------------------------------------------------
258
+
259
+ if (
260
+ componentData.dependencies &&
261
+ componentData.dependencies.length > 0
262
+ ) {
263
+ const depsToInstall =
264
+ componentData.dependencies.join(" ");
265
+
147
266
  s.start(
148
- `Installing missing dependencies: ${pc.cyan(depsToInstall)}...`
267
+ `Installing missing dependencies: ${pc.cyan(
268
+ depsToInstall,
269
+ )}...`,
149
270
  );
150
271
 
151
272
  try {
152
- // Silently runs the npm install command in the background
153
- execSync(`npm install ${depsToInstall}`, { stdio: "ignore" });
273
+ execSync(
274
+ `npm install ${depsToInstall}`,
275
+ {
276
+ stdio: "ignore",
277
+ },
278
+ );
279
+
154
280
  s.stop(
155
281
  pc.green(
156
- `Dependencies installed successfully: ${pc.gray(depsToInstall)}`
157
- )
282
+ `Dependencies installed successfully: ${pc.gray(
283
+ depsToInstall,
284
+ )}`,
285
+ ),
286
+ );
287
+ } catch {
288
+ s.stop(
289
+ pc.red(
290
+ "Failed to auto-install dependencies.",
291
+ ),
158
292
  );
159
- } catch (error) {
160
- s.stop(pc.red("Failed to auto-install dependencies."));
293
+
161
294
  note(
162
- `Please run: ${pc.cyan(`npm install ${depsToInstall}`)} manually.`,
163
- "Manual Action Required"
295
+ `Please run: ${pc.cyan(
296
+ `npm install ${depsToInstall}`,
297
+ )} manually.`,
298
+ "Manual Action Required",
164
299
  );
165
300
  }
166
301
  }
167
302
 
168
- // 7. Clean Success Outro
169
- // Find the main file to import (prioritize the one that matches the component name or use the first one)
170
- const mainFile = bundledFileNames.find((name: string) => name.toLowerCase() === componentName.toLowerCase()) || bundledFileNames[0];
171
-
303
+ // --------------------------------------------------
304
+ // 8. Success
305
+ // --------------------------------------------------
306
+
307
+ const mainFile =
308
+ bundledFileNames.find(
309
+ (name: string) =>
310
+ name.toLowerCase() ===
311
+ componentName.toLowerCase(),
312
+ ) || bundledFileNames[0];
313
+
172
314
  outro(
173
- `${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
174
- pc.gray(`Import it: `) +
315
+ `${pc.white("✔")} ${pc.bold(
316
+ `Component <${componentName} /> is ready!`,
317
+ )}\n` +
318
+ pc.gray("Import it: ") +
175
319
  pc.cyan(
176
- `import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`
177
- )
320
+ `import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`,
321
+ ),
178
322
  );
179
323
  } catch (error: any) {
180
- s.stop(pc.red("An error occurred during injection."));
181
- cancel(pc.gray(error.message || "Unknown CLI Error"));
324
+ s.stop(
325
+ pc.red(
326
+ "An error occurred during injection.",
327
+ ),
328
+ );
329
+
330
+ cancel(
331
+ pc.gray(
332
+ error?.message ||
333
+ "Unknown CLI Error",
334
+ ),
335
+ );
336
+
182
337
  process.exit(1);
183
338
  }
184
339
  });