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.
@@ -1 +1 @@
1
- {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkDpC,eAAO,MAAM,GAAG,SAgHZ,CAAC"}
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgEpC,eAAO,MAAM,GAAG,SAkRZ,CAAC"}
@@ -4,24 +4,24 @@ 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";
7
+ import { getToken } from "../utils/config.js";
8
+ import { signWithDeviceKey } from "../utils/secureKeyStore.js";
9
+ import machineIdPkg from "node-machine-id";
10
+ const { machineIdSync } = machineIdPkg;
8
11
  const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
9
12
  // Helper to verify Tailwind exists before doing anything
10
13
  function checkTailwindInstallation() {
11
14
  const targetDir = process.cwd();
12
15
  const pkgJsonPath = path.join(targetDir, "package.json");
13
- // 1. Ensure they are in a valid Node.js project
14
16
  if (!fs.existsSync(pkgJsonPath)) {
15
17
  cancel(pc.red("No package.json found. Please run this command inside a Node.js project."));
16
18
  process.exit(1);
17
19
  }
18
- // 2. Read package.json dependencies
19
20
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
20
21
  const allDeps = {
21
22
  ...pkgJson.dependencies,
22
23
  ...pkgJson.devDependencies,
23
24
  };
24
- // 3. Strict Tailwind Check
25
25
  if (!allDeps["tailwindcss"]) {
26
26
  cancel(pc.red("Tailwind CSS is missing from your project dependencies.\n") +
27
27
  pc.gray("DS01 components rely strictly on Tailwind CSS for styling.\n\n") +
@@ -37,10 +37,8 @@ export const add = new Command()
37
37
  .description("Add a component from DS01 to your project")
38
38
  .argument("<component>", "The name of the component (e.g., premium-section)")
39
39
  .action(async (componentName) => {
40
- console.log(); // Spacing for visual breathing room
41
- // 1. Premium Header
40
+ console.log();
42
41
  intro(`${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray("Adding Component")}`);
43
- // 2. Run Pre-Flight Tailwind Check
44
42
  checkTailwindInstallation();
45
43
  const cwd = process.cwd();
46
44
  const configPath = path.join(cwd, "ds01.config.json");
@@ -49,9 +47,8 @@ export const add = new Command()
49
47
  process.exit(1);
50
48
  }
51
49
  const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
52
- // Read the GLOBAL credentials, not the local project config
53
50
  const token = getToken();
54
- const machineId = getMachineId();
51
+ const machineId = machineIdSync();
55
52
  if (!token || !machineId) {
56
53
  cancel(pc.red("You are not authenticated. Run 'npx ds-01 login' first."));
57
54
  process.exit(1);
@@ -59,11 +56,31 @@ export const add = new Command()
59
56
  const s = spinner();
60
57
  s.start(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
61
58
  try {
62
- // 3. Fetch component from your API
59
+ // --------------------------------------------------
60
+ // 1. Create request-specific message
61
+ // --------------------------------------------------
62
+ const timestamp = Date.now().toString();
63
+ const message = [
64
+ "GET",
65
+ `/api/registry/${componentName}`,
66
+ timestamp,
67
+ machineId,
68
+ ].join("\n");
69
+ // --------------------------------------------------
70
+ // 2. Sign message using the device private key
71
+ // --------------------------------------------------
72
+ const signature = await signWithDeviceKey(new TextEncoder().encode(message));
73
+ const signatureBase64 = Buffer.from(signature).toString("base64");
74
+ // --------------------------------------------------
75
+ // 3. Send token + machine ID + signature
76
+ // --------------------------------------------------
63
77
  const response = await fetch(`${API_BASE_URL}/api/registry/${componentName}`, {
78
+ method: "GET",
64
79
  headers: {
65
80
  Authorization: `Bearer ${token}`,
66
- "X-Machine-ID": machineId, // <-- TypeScript error fixed here
81
+ "X-Machine-ID": machineId,
82
+ "X-DS01-Timestamp": timestamp,
83
+ "X-DS01-Signature": signatureBase64,
67
84
  },
68
85
  });
69
86
  if (!response.ok) {
@@ -72,39 +89,63 @@ export const add = new Command()
72
89
  process.exit(1);
73
90
  }
74
91
  const componentData = await response.json();
75
- // 4. Create dedicated component folder
92
+ // --------------------------------------------------
93
+ // 4. Create component folder
94
+ // --------------------------------------------------
76
95
  const targetDir = path.join(cwd, config.componentsPath, componentName);
77
96
  if (!fs.existsSync(targetDir)) {
78
- fs.mkdirSync(targetDir, { recursive: true });
97
+ fs.mkdirSync(targetDir, {
98
+ recursive: true,
99
+ });
79
100
  }
80
- // 5. Inject the pure code files
101
+ // --------------------------------------------------
102
+ // 5. Extract bundled file names
103
+ // --------------------------------------------------
104
+ const bundledFileNames = componentData.files.map((file) => file.name.replace(/\.[^/.]+$/, ""));
105
+ // --------------------------------------------------
106
+ // 6. Write component files
107
+ // --------------------------------------------------
81
108
  for (const file of componentData.files) {
82
109
  const filePath = path.join(targetDir, file.name);
83
- fs.writeFileSync(filePath, file.content);
110
+ let content = file.content;
111
+ bundledFileNames.forEach((fileName) => {
112
+ const regex = new RegExp(`from\\s+["']\\.[^"']*?\\/${fileName}["']`, "g");
113
+ content = content.replace(regex, `from "./${fileName}"`);
114
+ });
115
+ fs.writeFileSync(filePath, content);
84
116
  }
85
117
  s.stop(pc.green(`Downloaded ${componentData.files.length} files into ${pc.white(`/${config.componentsPath}/${componentName}`)}`));
86
- // 6. Auto-install Missing Component Dependencies (e.g., framer-motion)
87
- if (componentData.dependencies && componentData.dependencies.length > 0) {
118
+ // --------------------------------------------------
119
+ // 7. Install dependencies
120
+ // --------------------------------------------------
121
+ if (componentData.dependencies &&
122
+ componentData.dependencies.length > 0) {
88
123
  const depsToInstall = componentData.dependencies.join(" ");
89
124
  s.start(`Installing missing dependencies: ${pc.cyan(depsToInstall)}...`);
90
125
  try {
91
- // Silently runs the npm install command in the background
92
- execSync(`npm install ${depsToInstall}`, { stdio: "ignore" });
126
+ execSync(`npm install ${depsToInstall}`, {
127
+ stdio: "ignore",
128
+ });
93
129
  s.stop(pc.green(`Dependencies installed successfully: ${pc.gray(depsToInstall)}`));
94
130
  }
95
- catch (error) {
131
+ catch {
96
132
  s.stop(pc.red("Failed to auto-install dependencies."));
97
133
  note(`Please run: ${pc.cyan(`npm install ${depsToInstall}`)} manually.`, "Manual Action Required");
98
134
  }
99
135
  }
100
- // 7. Clean Success Outro
136
+ // --------------------------------------------------
137
+ // 8. Success
138
+ // --------------------------------------------------
139
+ const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
140
+ componentName.toLowerCase()) || bundledFileNames[0];
101
141
  outro(`${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
102
- pc.gray(`Import it: `) +
103
- pc.cyan(`import { Section } from "@/${config.componentsPath}/${componentName}/Section"`));
142
+ pc.gray("Import it: ") +
143
+ pc.cyan(`import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`));
104
144
  }
105
145
  catch (error) {
106
146
  s.stop(pc.red("An error occurred during injection."));
107
- cancel(pc.gray(error.message || "Unknown CLI Error"));
147
+ cancel(pc.gray(error?.message ||
148
+ "Unknown CLI Error"));
108
149
  process.exit(1);
109
150
  }
110
151
  });
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,eAAO,MAAM,KAAK,SA4Fd,CAAC"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,eAAO,MAAM,KAAK,SA2Id,CAAC"}
@@ -4,24 +4,40 @@ import pc from "picocolors";
4
4
  import open from "open";
5
5
  import machineIdPkg from "node-machine-id";
6
6
  import { saveToken } from "../utils/config.js";
7
+ import { createDeviceKey, getDevicePublicKey, } from "../utils/secureKeyStore.js";
7
8
  const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
8
9
  const { machineIdSync } = machineIdPkg;
9
10
  export const login = new Command()
10
11
  .name("login")
11
12
  .description("Authenticate your terminal with DS01 via browser")
12
13
  .action(async () => {
13
- console.log(); // Spacing for breathing room
14
- // 1. Header banner
14
+ console.log();
15
15
  intro(`${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray("v1.0.0 — Terminal Authentication")}`);
16
16
  const s = spinner();
17
17
  try {
18
- // 2. Hardware ID Generation & Device Code Request
19
- s.start("Generating hardware fingerprint & requesting pairing code...");
18
+ // --------------------------------------------------
19
+ // 1. Create / load the device's secure key
20
+ // --------------------------------------------------
21
+ s.start("Preparing secure device identity...");
22
+ await createDeviceKey();
23
+ // Only the PUBLIC certificate/key leaves the machine.
24
+ const publicKey = await getDevicePublicKey();
25
+ // Existing machine fingerprint.
20
26
  const hardwareId = machineIdSync();
27
+ // --------------------------------------------------
28
+ // 2. Start device authorization
29
+ // --------------------------------------------------
30
+ s.start("Requesting pairing code...");
21
31
  const initResponse = await fetch(`${API_BASE_URL}/api/auth/device/code`, {
22
32
  method: "POST",
23
- headers: { "Content-Type": "application/json" },
24
- body: JSON.stringify({ machineId: hardwareId }),
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ },
36
+ body: JSON.stringify({
37
+ machineId: hardwareId,
38
+ publicKey,
39
+ publicKeyType: process.platform === "win32" ? "windows-rsa" : "macos-ec",
40
+ }),
25
41
  });
26
42
  if (!initResponse.ok) {
27
43
  s.stop(pc.red("Failed to reach DS01 authorization server."));
@@ -29,18 +45,24 @@ export const login = new Command()
29
45
  process.exit(1);
30
46
  }
31
47
  const { deviceCode, userCode, verificationUrl, interval } = await initResponse.json();
32
- s.stop(pc.green("Device authorization code generated."));
33
- // 3. Highlighted Box for User Action
48
+ if (!deviceCode || !userCode || !verificationUrl) {
49
+ throw new Error("Authorization server returned an invalid device response.");
50
+ }
51
+ s.stop(pc.green("Secure device identity registered."));
52
+ // --------------------------------------------------
53
+ // 3. Ask user to approve in browser
54
+ // --------------------------------------------------
34
55
  note(`${pc.bold("Pairing Code:")} ${pc.cyan(pc.bold(` ${userCode} `))}\n` +
35
56
  `${pc.bold("Verification URL:")} ${pc.underline(verificationUrl)}`, "Action Required");
36
- // 4. Auto-launch Browser
37
57
  try {
38
58
  await open(verificationUrl);
39
59
  }
40
60
  catch {
41
- // Non-blocking fallback if browser launch fails on headless setups
61
+ // Browser launch failure is non-fatal.
42
62
  }
43
- // 5. Polling Loop
63
+ // --------------------------------------------------
64
+ // 4. Poll for authorization
65
+ // --------------------------------------------------
44
66
  s.start("Waiting for web authorization...");
45
67
  let token = null;
46
68
  const pollInterval = (interval || 5) * 1000;
@@ -48,8 +70,13 @@ export const login = new Command()
48
70
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
49
71
  const tokenResponse = await fetch(`${API_BASE_URL}/api/auth/device/token`, {
50
72
  method: "POST",
51
- headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify({ deviceCode, machineId: hardwareId }),
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ },
76
+ body: JSON.stringify({
77
+ deviceCode,
78
+ machineId: hardwareId,
79
+ }),
53
80
  });
54
81
  const tokenData = await tokenResponse.json();
55
82
  if (tokenResponse.ok && tokenData.accessToken) {
@@ -61,15 +88,17 @@ export const login = new Command()
61
88
  process.exit(1);
62
89
  }
63
90
  }
64
- // 6. Save token to local wallet
91
+ // --------------------------------------------------
92
+ // 5. Save CLI token + machine ID
93
+ // --------------------------------------------------
65
94
  saveToken(token, hardwareId);
66
95
  s.stop(pc.green("Hardware signature linked & token verified!"));
67
- // 7. Clean Success Outro
68
- outro(`${pc.white("✔")} ${pc.bold("Terminal synced successfully")} ${pc.gray("Session token saved locally.")}`);
96
+ outro(`${pc.white("✔")} ${pc.bold("Terminal synced successfully")}\n` +
97
+ pc.gray("Secure device key and session token are ready."));
69
98
  }
70
99
  catch (error) {
71
100
  s.stop(pc.red("An error occurred during login."));
72
- cancel(pc.gray(error.message || "Unknown CLI Error"));
101
+ cancel(pc.gray(error?.message || "Unknown CLI Error"));
73
102
  process.exit(1);
74
103
  }
75
104
  });
@@ -1 +1 @@
1
- {"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,MAAM,SAuBf,CAAC"}
1
+ {"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,eAAO,MAAM,MAAM,SAiDf,CAAC"}
@@ -2,24 +2,44 @@ import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import ora from "ora";
4
4
  import { deleteToken, getToken } from "../utils/config.js";
5
+ import machineIdPkg from "node-machine-id";
6
+ const { machineIdSync } = machineIdPkg;
7
+ const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
5
8
  export const logout = new Command()
6
9
  .name("logout")
7
- .description("Log out of your DS01 account and clear local credentials")
10
+ .description("Log out of your DS01 account and revoke the CLI session")
8
11
  .action(async () => {
9
12
  const token = getToken();
10
- if (!token) {
13
+ const machineId = machineIdSync();
14
+ if (!token || !machineId) {
11
15
  console.log(chalk.yellow("You are already logged out."));
12
16
  return;
13
17
  }
14
- const spinner = ora("Logging out...").start();
18
+ const spinner = ora("Revoking CLI session...").start();
15
19
  try {
16
- // Throw away the digital wallet / token
20
+ const response = await fetch(`${API_BASE_URL}/api/auth/cli/logout`, {
21
+ method: "POST",
22
+ headers: {
23
+ Authorization: `Bearer ${token}`,
24
+ "X-Machine-ID": machineId,
25
+ },
26
+ });
27
+ // NEW: Do not silently delete local credentials if the server
28
+ // failed to revoke the session.
29
+ if (!response.ok) {
30
+ const data = await response.json().catch(() => null);
31
+ throw new Error(data?.error || `Logout failed (HTTP ${response.status})`);
32
+ }
33
+ // NEW: Delete local credentials only after the server confirms
34
+ // that the CLI session has been revoked.
35
+ console.log("✅ Server confirmed CLI session was revoked.");
17
36
  deleteToken();
37
+ console.log("✅ Local credentials deleted.");
18
38
  spinner.succeed(chalk.green("Successfully logged out."));
19
- console.log(chalk.gray("Your local credentials have been cleared."));
39
+ console.log(chalk.gray("Your CLI session has been revoked and local credentials have been cleared."));
20
40
  }
21
41
  catch (error) {
22
- spinner.fail(chalk.red("Failed to log out cleanly."));
23
- console.error(error);
42
+ spinner.fail(chalk.red("Failed to log out."));
43
+ console.error(chalk.red(error.message || "Unknown logout error."));
24
44
  }
25
45
  });
package/dist/index.js CHANGED
@@ -17,4 +17,6 @@ program.addCommand(logout);
17
17
  program.addCommand(init);
18
18
  program.addCommand(add); // <-- 2. Register the add command!
19
19
  // Parse the arguments from the terminal
20
+ // It is the point where Commander reads the user's terminal input and decides
21
+ // which registered command's .action() function should execute.
20
22
  program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-key.d.ts","sourceRoot":"","sources":["../src/test-key.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import { createDeviceKey, getDevicePublicKey, signWithDeviceKey, } from "./utils/secureKeyStore.js";
2
+ await createDeviceKey();
3
+ const publicKey = await getDevicePublicKey();
4
+ console.log("Public key:", publicKey);
5
+ const message = new TextEncoder().encode("hello DS01");
6
+ const signature = await signWithDeviceKey(message);
7
+ console.log("Signature:", Buffer.from(signature).toString("base64"));
@@ -1,5 +1,4 @@
1
1
  export declare function saveToken(token: string, machineId: string): void;
2
2
  export declare function getToken(): string | null;
3
- export declare function getMachineId(): string | null;
4
3
  export declare function deleteToken(): void;
5
4
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAQA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAczD;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAWxC;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAW5C;AAED,wBAAgB,WAAW,SAI1B"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAQA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAczD;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAWxC;AAID,wBAAgB,WAAW,SAI1B"}
@@ -30,19 +30,6 @@ export function getToken() {
30
30
  }
31
31
  return null;
32
32
  }
33
- export function getMachineId() {
34
- if (fs.existsSync(credentialsPath)) {
35
- try {
36
- const data = fs.readFileSync(credentialsPath, "utf-8");
37
- const parsed = JSON.parse(data);
38
- return parsed.machineId || null;
39
- }
40
- catch {
41
- return null;
42
- }
43
- }
44
- return null;
45
- }
46
33
  export function deleteToken() {
47
34
  if (fs.existsSync(credentialsPath)) {
48
35
  fs.unlinkSync(credentialsPath);
@@ -0,0 +1,3 @@
1
+ import { webcrypto } from "node:crypto";
2
+ export declare function generateDeviceKeyPair(): Promise<webcrypto.CryptoKeyPair>;
3
+ //# sourceMappingURL=deviceKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deviceKey.d.ts","sourceRoot":"","sources":["../../src/utils/deviceKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC,wBAAsB,qBAAqB,qCAc1C"}
@@ -0,0 +1,13 @@
1
+ import { webcrypto } from "node:crypto";
2
+ const { subtle } = webcrypto;
3
+ export async function generateDeviceKeyPair() {
4
+ // Generate an asymmetric key pair:
5
+ // - publicKey → can be shared with the server
6
+ // - privateKey → stays on this device
7
+ const keyPair = await subtle.generateKey({
8
+ name: "ECDSA",
9
+ namedCurve: "P-256",
10
+ }, false, // 🔐 Private key is NON-EXPORTABLE
11
+ ["sign", "verify"]);
12
+ return keyPair;
13
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ---------------------------------------------------------
3
+ * PUBLIC API
4
+ * ---------------------------------------------------------
5
+ */
6
+ export declare function createDeviceKey(): Promise<void>;
7
+ export declare function getDevicePublicKey(): Promise<string>;
8
+ export declare function signWithDeviceKey(data: Uint8Array): Promise<Uint8Array>;
9
+ export declare function deleteDeviceKey(): Promise<void>;
10
+ //# sourceMappingURL=secureKeyStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AAwTA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}