ds-01 1.0.3 → 1.0.5

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.3",
3
+ "version": "1.0.5",
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,113 +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
- // 5. Inject the pure code files
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
+ // --------------------------------------------------
218
+
115
219
  for (const file of componentData.files) {
116
- const filePath = path.join(targetDir, file.name);
117
- fs.writeFileSync(filePath, file.content);
220
+ const filePath = path.join(
221
+ targetDir,
222
+ file.name,
223
+ );
224
+
225
+ let content = file.content;
226
+
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
+ );
240
+
241
+ fs.writeFileSync(
242
+ filePath,
243
+ content,
244
+ );
118
245
  }
119
246
 
120
247
  s.stop(
121
248
  pc.green(
122
- `Downloaded ${componentData.files.length} files into ${pc.white(`/${config.componentsPath}/${componentName}`)}`
123
- )
249
+ `Downloaded ${componentData.files.length} files into ${pc.white(
250
+ `/${config.componentsPath}/${componentName}`,
251
+ )}`,
252
+ ),
124
253
  );
125
254
 
126
- // 6. Auto-install Missing Component Dependencies (e.g., framer-motion)
127
- if (componentData.dependencies && componentData.dependencies.length > 0) {
128
- 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
+
129
266
  s.start(
130
- `Installing missing dependencies: ${pc.cyan(depsToInstall)}...`
267
+ `Installing missing dependencies: ${pc.cyan(
268
+ depsToInstall,
269
+ )}...`,
131
270
  );
132
271
 
133
272
  try {
134
- // Silently runs the npm install command in the background
135
- execSync(`npm install ${depsToInstall}`, { stdio: "ignore" });
273
+ execSync(
274
+ `npm install ${depsToInstall}`,
275
+ {
276
+ stdio: "ignore",
277
+ },
278
+ );
279
+
136
280
  s.stop(
137
281
  pc.green(
138
- `Dependencies installed successfully: ${pc.gray(depsToInstall)}`
139
- )
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
+ ),
140
292
  );
141
- } catch (error) {
142
- s.stop(pc.red("Failed to auto-install dependencies."));
293
+
143
294
  note(
144
- `Please run: ${pc.cyan(`npm install ${depsToInstall}`)} manually.`,
145
- "Manual Action Required"
295
+ `Please run: ${pc.cyan(
296
+ `npm install ${depsToInstall}`,
297
+ )} manually.`,
298
+ "Manual Action Required",
146
299
  );
147
300
  }
148
301
  }
149
302
 
150
- // 7. Clean Success Outro
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
+
151
314
  outro(
152
- `${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
153
- pc.gray(`Import it: `) +
315
+ `${pc.white("✔")} ${pc.bold(
316
+ `Component <${componentName} /> is ready!`,
317
+ )}\n` +
318
+ pc.gray("Import it: ") +
154
319
  pc.cyan(
155
- `import { Section } from "@/${config.componentsPath}/${componentName}/Section"`
156
- )
320
+ `import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`,
321
+ ),
157
322
  );
158
323
  } catch (error: any) {
159
- s.stop(pc.red("An error occurred during injection."));
160
- 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
+
161
337
  process.exit(1);
162
338
  }
163
339
  });