ds-01 1.0.9 → 1.0.11
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/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +63 -14
- package/dist/utils/secureKeyStore.d.ts.map +1 -1
- package/dist/utils/secureKeyStore.js +18 -57
- package/package.json +1 -1
- package/src/commands/add.ts +209 -52
- package/src/utils/secureKeyStore.ts +18 -58
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsGpC,eAAO,MAAM,GAAG,SAwTZ,CAAC"}
|
package/dist/commands/add.js
CHANGED
|
@@ -9,7 +9,9 @@ import { signWithDeviceKey } from "../utils/secureKeyStore.js";
|
|
|
9
9
|
import machineIdPkg from "node-machine-id";
|
|
10
10
|
const { machineIdSync } = machineIdPkg;
|
|
11
11
|
const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
12
|
-
//
|
|
12
|
+
// ---------------------------------------------------------
|
|
13
|
+
// Helper: Verify Tailwind exists
|
|
14
|
+
// ---------------------------------------------------------
|
|
13
15
|
function checkTailwindInstallation() {
|
|
14
16
|
const targetDir = process.cwd();
|
|
15
17
|
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
@@ -32,6 +34,24 @@ function checkTailwindInstallation() {
|
|
|
32
34
|
process.exit(1);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
// ---------------------------------------------------------
|
|
38
|
+
// Helper: Find dependencies that are actually missing
|
|
39
|
+
// ---------------------------------------------------------
|
|
40
|
+
function getMissingDependencies(dependencies) {
|
|
41
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
42
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
43
|
+
return dependencies;
|
|
44
|
+
}
|
|
45
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
46
|
+
const installedDependencies = {
|
|
47
|
+
...(packageJson.dependencies || {}),
|
|
48
|
+
...(packageJson.devDependencies || {}),
|
|
49
|
+
};
|
|
50
|
+
return dependencies.filter((dependency) => !installedDependencies[dependency]);
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------
|
|
53
|
+
// ADD COMMAND
|
|
54
|
+
// ---------------------------------------------------------
|
|
35
55
|
export const add = new Command()
|
|
36
56
|
.name("add")
|
|
37
57
|
.description("Add a component from DS01 to your project")
|
|
@@ -39,6 +59,9 @@ export const add = new Command()
|
|
|
39
59
|
.action(async (componentName) => {
|
|
40
60
|
console.log();
|
|
41
61
|
intro(`${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray("Adding Component")}`);
|
|
62
|
+
// -----------------------------------------------------
|
|
63
|
+
// 1. Validate project
|
|
64
|
+
// -----------------------------------------------------
|
|
42
65
|
checkTailwindInstallation();
|
|
43
66
|
const cwd = process.cwd();
|
|
44
67
|
const configPath = path.join(cwd, "ds01.config.json");
|
|
@@ -47,6 +70,9 @@ export const add = new Command()
|
|
|
47
70
|
process.exit(1);
|
|
48
71
|
}
|
|
49
72
|
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
73
|
+
// -----------------------------------------------------
|
|
74
|
+
// 2. Authentication
|
|
75
|
+
// -----------------------------------------------------
|
|
50
76
|
const token = getToken();
|
|
51
77
|
const machineId = machineIdSync();
|
|
52
78
|
if (!token || !machineId) {
|
|
@@ -57,7 +83,7 @@ export const add = new Command()
|
|
|
57
83
|
s.start(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
|
|
58
84
|
try {
|
|
59
85
|
// --------------------------------------------------
|
|
60
|
-
//
|
|
86
|
+
// 3. Create request-specific signed message
|
|
61
87
|
// --------------------------------------------------
|
|
62
88
|
const timestamp = Date.now().toString();
|
|
63
89
|
const message = [
|
|
@@ -67,12 +93,12 @@ export const add = new Command()
|
|
|
67
93
|
machineId,
|
|
68
94
|
].join("\n");
|
|
69
95
|
// --------------------------------------------------
|
|
70
|
-
//
|
|
96
|
+
// 4. Sign request with secure device private key
|
|
71
97
|
// --------------------------------------------------
|
|
72
98
|
const signature = await signWithDeviceKey(new TextEncoder().encode(message));
|
|
73
99
|
const signatureBase64 = Buffer.from(signature).toString("base64");
|
|
74
100
|
// --------------------------------------------------
|
|
75
|
-
//
|
|
101
|
+
// 5. Request component from registry
|
|
76
102
|
// --------------------------------------------------
|
|
77
103
|
const response = await fetch(`${API_BASE_URL}/api/registry/${componentName}`, {
|
|
78
104
|
method: "GET",
|
|
@@ -83,15 +109,19 @@ export const add = new Command()
|
|
|
83
109
|
"X-DS01-Signature": signatureBase64,
|
|
84
110
|
},
|
|
85
111
|
});
|
|
112
|
+
// --------------------------------------------------
|
|
113
|
+
// 6. Handle registry errors
|
|
114
|
+
// --------------------------------------------------
|
|
86
115
|
if (!response.ok) {
|
|
87
116
|
const errorData = await response.json().catch(() => null);
|
|
88
117
|
s.stop(pc.red("Component fetch failed."));
|
|
89
|
-
cancel(errorData?.error ||
|
|
118
|
+
cancel(errorData?.error ||
|
|
119
|
+
`Server returned HTTP ${response.status}`);
|
|
90
120
|
process.exit(1);
|
|
91
121
|
}
|
|
92
122
|
const componentData = await response.json();
|
|
93
123
|
// --------------------------------------------------
|
|
94
|
-
//
|
|
124
|
+
// 7. Create component directory
|
|
95
125
|
// --------------------------------------------------
|
|
96
126
|
const targetDir = path.join(cwd, config.componentsPath, componentName);
|
|
97
127
|
if (!fs.existsSync(targetDir)) {
|
|
@@ -100,11 +130,11 @@ export const add = new Command()
|
|
|
100
130
|
});
|
|
101
131
|
}
|
|
102
132
|
// --------------------------------------------------
|
|
103
|
-
//
|
|
133
|
+
// 8. Extract bundled file names
|
|
104
134
|
// --------------------------------------------------
|
|
105
135
|
const bundledFileNames = componentData.files.map((file) => file.name.replace(/\.[^/.]+$/, ""));
|
|
106
136
|
// --------------------------------------------------
|
|
107
|
-
//
|
|
137
|
+
// 9. Write component files
|
|
108
138
|
// --------------------------------------------------
|
|
109
139
|
for (const file of componentData.files) {
|
|
110
140
|
const filePath = path.join(targetDir, file.name);
|
|
@@ -117,10 +147,14 @@ export const add = new Command()
|
|
|
117
147
|
}
|
|
118
148
|
s.stop(pc.green(`Downloaded ${componentData.files.length} files into ${pc.white(`/${config.componentsPath}/${componentName}`)}`));
|
|
119
149
|
// --------------------------------------------------
|
|
120
|
-
//
|
|
150
|
+
// 10. Install ONLY missing dependencies
|
|
121
151
|
// --------------------------------------------------
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
const registryDependencies = Array.isArray(componentData.dependencies)
|
|
153
|
+
? componentData.dependencies
|
|
154
|
+
: [];
|
|
155
|
+
const missingDependencies = getMissingDependencies(registryDependencies);
|
|
156
|
+
if (missingDependencies.length > 0) {
|
|
157
|
+
const depsToInstall = missingDependencies.join(" ");
|
|
124
158
|
s.start(`Installing missing dependencies: ${pc.cyan(depsToInstall)}...`);
|
|
125
159
|
try {
|
|
126
160
|
execSync(`npm install ${depsToInstall}`, {
|
|
@@ -134,16 +168,31 @@ export const add = new Command()
|
|
|
134
168
|
}
|
|
135
169
|
}
|
|
136
170
|
// --------------------------------------------------
|
|
137
|
-
//
|
|
171
|
+
// 11. No dependencies needed
|
|
172
|
+
// --------------------------------------------------
|
|
173
|
+
// We intentionally don't show a spinner here.
|
|
174
|
+
//
|
|
175
|
+
// Example:
|
|
176
|
+
//
|
|
177
|
+
// DS01 requires ["motion"]
|
|
178
|
+
// package.json already contains "motion"
|
|
179
|
+
//
|
|
180
|
+
// → Nothing happens.
|
|
181
|
+
// → No npm install.
|
|
182
|
+
// → No unnecessary package changes.
|
|
183
|
+
// --------------------------------------------------
|
|
184
|
+
// 12. Success
|
|
138
185
|
// --------------------------------------------------
|
|
139
|
-
const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
|
|
186
|
+
const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
|
|
187
|
+
componentName.toLowerCase()) || bundledFileNames[0];
|
|
140
188
|
outro(`${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
|
|
141
189
|
pc.gray("Import it: ") +
|
|
142
190
|
pc.cyan(`import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`));
|
|
143
191
|
}
|
|
144
192
|
catch (error) {
|
|
145
193
|
s.stop(pc.red("An error occurred during injection."));
|
|
146
|
-
cancel(pc.gray(error?.message ||
|
|
194
|
+
cancel(pc.gray(error?.message ||
|
|
195
|
+
"Unknown CLI Error"));
|
|
147
196
|
process.exit(1);
|
|
148
197
|
}
|
|
149
198
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AA2WA;;;;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"}
|
|
@@ -9,8 +9,6 @@ const KEY_NAME = "DS01-CLI-Device-Key";
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
11
|
function getMacHelperPath() {
|
|
12
|
-
// dist/utils/secureKeyStore.js
|
|
13
|
-
// ../native/macos-key-helper.swift
|
|
14
12
|
return path.resolve(__dirname, "../native/macos-key-helper.swift");
|
|
15
13
|
}
|
|
16
14
|
function ensureSupportedOS() {
|
|
@@ -23,52 +21,16 @@ function ensureSupportedOS() {
|
|
|
23
21
|
* ---------------------------------------------------------
|
|
24
22
|
* WINDOWS
|
|
25
23
|
* ---------------------------------------------------------
|
|
26
|
-
*
|
|
27
|
-
* Windows uses:
|
|
28
|
-
*
|
|
29
|
-
* Microsoft Platform Crypto Provider
|
|
30
|
-
* RSA 2048
|
|
31
|
-
* NonExportable private key
|
|
32
|
-
*
|
|
33
|
-
* The private key never enters Node.js.
|
|
34
|
-
*
|
|
35
|
-
* Node -> PowerShell -> Windows crypto provider
|
|
36
|
-
* ↓
|
|
37
|
-
* private key
|
|
38
|
-
* ↓
|
|
39
|
-
* signature
|
|
40
|
-
* ↓
|
|
41
|
-
* Node
|
|
42
|
-
*
|
|
43
|
-
* IMPORTANT:
|
|
44
|
-
*
|
|
45
|
-
* We identify the certificate using its properties rather
|
|
46
|
-
* than blindly taking the first certificate with the same
|
|
47
|
-
* subject.
|
|
48
24
|
*/
|
|
49
|
-
/**
|
|
50
|
-
* PowerShell expression used to identify a valid DS01
|
|
51
|
-
* certificate.
|
|
52
|
-
*
|
|
53
|
-
* Requirements:
|
|
54
|
-
*
|
|
55
|
-
* - Correct subject
|
|
56
|
-
* - Has private key
|
|
57
|
-
* - RSA certificate
|
|
58
|
-
* - Platform Crypto Provider
|
|
59
|
-
*/
|
|
60
|
-
const WINDOWS_CERT_FILTER = `
|
|
61
|
-
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
62
|
-
$_.HasPrivateKey -and
|
|
63
|
-
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
64
|
-
`;
|
|
65
25
|
async function createWindowsDeviceKey() {
|
|
66
26
|
const script = `
|
|
67
27
|
$ErrorActionPreference = "Stop"
|
|
68
28
|
|
|
69
29
|
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
70
30
|
Where-Object {
|
|
71
|
-
${
|
|
31
|
+
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
32
|
+
$_.HasPrivateKey -and
|
|
33
|
+
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
72
34
|
})
|
|
73
35
|
|
|
74
36
|
if ($certificates.Count -gt 1) {
|
|
@@ -82,7 +44,6 @@ if ($certificates.Count -eq 1) {
|
|
|
82
44
|
|
|
83
45
|
$params = @{
|
|
84
46
|
Type = "Custom"
|
|
85
|
-
|
|
86
47
|
Subject = "CN=${KEY_NAME}"
|
|
87
48
|
|
|
88
49
|
Provider = "Microsoft Platform Crypto Provider"
|
|
@@ -90,7 +51,6 @@ $params = @{
|
|
|
90
51
|
KeyAlgorithm = "RSA"
|
|
91
52
|
KeyLength = 2048
|
|
92
53
|
|
|
93
|
-
# Private key cannot be exported.
|
|
94
54
|
KeyExportPolicy = "NonExportable"
|
|
95
55
|
|
|
96
56
|
KeyUsage = "DigitalSignature"
|
|
@@ -125,18 +85,21 @@ Write-Output "CREATED"
|
|
|
125
85
|
}
|
|
126
86
|
}
|
|
127
87
|
/**
|
|
128
|
-
* Find
|
|
88
|
+
* Find exactly one valid DS01 certificate.
|
|
129
89
|
*
|
|
130
|
-
* We
|
|
90
|
+
* We intentionally do not use Select-Object -First 1.
|
|
131
91
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
92
|
+
* 0 certificates -> error
|
|
93
|
+
* 1 certificate -> use it
|
|
94
|
+
* 2+ certificates -> error
|
|
134
95
|
*/
|
|
135
|
-
function
|
|
96
|
+
function getWindowsCertificateLookup() {
|
|
136
97
|
return `
|
|
137
98
|
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
138
99
|
Where-Object {
|
|
139
|
-
${
|
|
100
|
+
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
101
|
+
$_.HasPrivateKey -and
|
|
102
|
+
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
140
103
|
})
|
|
141
104
|
|
|
142
105
|
if ($certificates.Count -eq 0) {
|
|
@@ -147,19 +110,15 @@ if ($certificates.Count -gt 1) {
|
|
|
147
110
|
throw "Multiple valid DS01 device certificates found."
|
|
148
111
|
}
|
|
149
112
|
|
|
150
|
-
$certificates[0]
|
|
113
|
+
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificates[0]
|
|
151
114
|
`;
|
|
152
115
|
}
|
|
153
116
|
async function getWindowsPublicKey() {
|
|
154
117
|
const script = `
|
|
155
118
|
$ErrorActionPreference = "Stop"
|
|
156
119
|
|
|
157
|
-
$
|
|
120
|
+
${getWindowsCertificateLookup()}
|
|
158
121
|
|
|
159
|
-
# Return the complete public certificate.
|
|
160
|
-
#
|
|
161
|
-
# The certificate contains the public key and certificate
|
|
162
|
-
# metadata, but never the private key.
|
|
163
122
|
[Convert]::ToBase64String($cert.RawData)
|
|
164
123
|
`;
|
|
165
124
|
const { stdout } = await execFileAsync("powershell.exe", [
|
|
@@ -183,7 +142,7 @@ async function signWindowsDeviceKey(data) {
|
|
|
183
142
|
const script = `
|
|
184
143
|
$ErrorActionPreference = "Stop"
|
|
185
144
|
|
|
186
|
-
$
|
|
145
|
+
${getWindowsCertificateLookup()}
|
|
187
146
|
|
|
188
147
|
if (-not $cert.HasPrivateKey) {
|
|
189
148
|
throw "DS01 device certificate has no private key."
|
|
@@ -233,7 +192,9 @@ $ErrorActionPreference = "Stop"
|
|
|
233
192
|
|
|
234
193
|
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
235
194
|
Where-Object {
|
|
236
|
-
${
|
|
195
|
+
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
196
|
+
$_.HasPrivateKey -and
|
|
197
|
+
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
237
198
|
})
|
|
238
199
|
|
|
239
200
|
if ($certificates.Count -gt 1) {
|
package/package.json
CHANGED
package/src/commands/add.ts
CHANGED
|
@@ -12,9 +12,13 @@ import machineIdPkg from "node-machine-id";
|
|
|
12
12
|
|
|
13
13
|
const { machineIdSync } = machineIdPkg;
|
|
14
14
|
|
|
15
|
-
const API_BASE_URL =
|
|
15
|
+
const API_BASE_URL =
|
|
16
|
+
process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------
|
|
19
|
+
// Helper: Verify Tailwind exists
|
|
20
|
+
// ---------------------------------------------------------
|
|
16
21
|
|
|
17
|
-
// Helper to verify Tailwind exists before doing anything
|
|
18
22
|
function checkTailwindInstallation() {
|
|
19
23
|
const targetDir = process.cwd();
|
|
20
24
|
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
@@ -25,10 +29,13 @@ function checkTailwindInstallation() {
|
|
|
25
29
|
"No package.json found. Please run this command inside a Node.js project.",
|
|
26
30
|
),
|
|
27
31
|
);
|
|
32
|
+
|
|
28
33
|
process.exit(1);
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
const pkgJson = JSON.parse(
|
|
36
|
+
const pkgJson = JSON.parse(
|
|
37
|
+
fs.readFileSync(pkgJsonPath, "utf-8"),
|
|
38
|
+
);
|
|
32
39
|
|
|
33
40
|
const allDeps = {
|
|
34
41
|
...pkgJson.dependencies,
|
|
@@ -37,26 +44,69 @@ function checkTailwindInstallation() {
|
|
|
37
44
|
|
|
38
45
|
if (!allDeps["tailwindcss"]) {
|
|
39
46
|
cancel(
|
|
40
|
-
pc.red(
|
|
47
|
+
pc.red(
|
|
48
|
+
"Tailwind CSS is missing from your project dependencies.\n",
|
|
49
|
+
) +
|
|
41
50
|
pc.gray(
|
|
42
51
|
"DS01 components rely strictly on Tailwind CSS for styling.\n\n",
|
|
43
52
|
) +
|
|
44
53
|
pc.white(
|
|
45
54
|
"Kindly install it and configure your project, then retry:\n",
|
|
46
55
|
) +
|
|
47
|
-
pc.cyan(
|
|
56
|
+
pc.cyan(
|
|
57
|
+
" npm install tailwindcss @tailwindcss/postcss postcss\n\n",
|
|
58
|
+
) +
|
|
48
59
|
pc.gray("Official Setup Guide: ") +
|
|
49
|
-
pc.underline(
|
|
60
|
+
pc.underline(
|
|
61
|
+
"https://tailwindcss.com/docs/installation",
|
|
62
|
+
),
|
|
50
63
|
);
|
|
51
64
|
|
|
52
65
|
process.exit(1);
|
|
53
66
|
}
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
// ---------------------------------------------------------
|
|
70
|
+
// Helper: Find dependencies that are actually missing
|
|
71
|
+
// ---------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function getMissingDependencies(
|
|
74
|
+
dependencies: string[],
|
|
75
|
+
): string[] {
|
|
76
|
+
const packageJsonPath = path.join(
|
|
77
|
+
process.cwd(),
|
|
78
|
+
"package.json",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
82
|
+
return dependencies;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const packageJson = JSON.parse(
|
|
86
|
+
fs.readFileSync(packageJsonPath, "utf-8"),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const installedDependencies = {
|
|
90
|
+
...(packageJson.dependencies || {}),
|
|
91
|
+
...(packageJson.devDependencies || {}),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return dependencies.filter(
|
|
95
|
+
(dependency) => !installedDependencies[dependency],
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------
|
|
100
|
+
// ADD COMMAND
|
|
101
|
+
// ---------------------------------------------------------
|
|
102
|
+
|
|
56
103
|
export const add = new Command()
|
|
57
104
|
.name("add")
|
|
58
105
|
.description("Add a component from DS01 to your project")
|
|
59
|
-
.argument(
|
|
106
|
+
.argument(
|
|
107
|
+
"<component>",
|
|
108
|
+
"The name of the component (e.g., premium-section)",
|
|
109
|
+
)
|
|
60
110
|
.action(async (componentName: string) => {
|
|
61
111
|
console.log();
|
|
62
112
|
|
|
@@ -66,36 +116,61 @@ export const add = new Command()
|
|
|
66
116
|
)}`,
|
|
67
117
|
);
|
|
68
118
|
|
|
119
|
+
// -----------------------------------------------------
|
|
120
|
+
// 1. Validate project
|
|
121
|
+
// -----------------------------------------------------
|
|
122
|
+
|
|
69
123
|
checkTailwindInstallation();
|
|
70
124
|
|
|
71
125
|
const cwd = process.cwd();
|
|
72
126
|
|
|
73
|
-
const configPath = path.join(
|
|
127
|
+
const configPath = path.join(
|
|
128
|
+
cwd,
|
|
129
|
+
"ds01.config.json",
|
|
130
|
+
);
|
|
74
131
|
|
|
75
132
|
if (!fs.existsSync(configPath)) {
|
|
76
|
-
cancel(
|
|
133
|
+
cancel(
|
|
134
|
+
pc.red(
|
|
135
|
+
"ds01.config.json not found. Run 'npx ds-01 init' first.",
|
|
136
|
+
),
|
|
137
|
+
);
|
|
77
138
|
|
|
78
139
|
process.exit(1);
|
|
79
140
|
}
|
|
80
141
|
|
|
81
|
-
const config = JSON.parse(
|
|
142
|
+
const config = JSON.parse(
|
|
143
|
+
fs.readFileSync(configPath, "utf-8"),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// -----------------------------------------------------
|
|
147
|
+
// 2. Authentication
|
|
148
|
+
// -----------------------------------------------------
|
|
82
149
|
|
|
83
150
|
const token = getToken();
|
|
84
151
|
const machineId = machineIdSync();
|
|
85
152
|
|
|
86
153
|
if (!token || !machineId) {
|
|
87
|
-
cancel(
|
|
154
|
+
cancel(
|
|
155
|
+
pc.red(
|
|
156
|
+
"You are not authenticated. Run 'npx ds-01 login' first.",
|
|
157
|
+
),
|
|
158
|
+
);
|
|
88
159
|
|
|
89
160
|
process.exit(1);
|
|
90
161
|
}
|
|
91
162
|
|
|
92
163
|
const s = spinner();
|
|
93
164
|
|
|
94
|
-
s.start(
|
|
165
|
+
s.start(
|
|
166
|
+
`Fetching ${pc.cyan(
|
|
167
|
+
`<${componentName} />`,
|
|
168
|
+
)} from registry...`,
|
|
169
|
+
);
|
|
95
170
|
|
|
96
171
|
try {
|
|
97
172
|
// --------------------------------------------------
|
|
98
|
-
//
|
|
173
|
+
// 3. Create request-specific signed message
|
|
99
174
|
// --------------------------------------------------
|
|
100
175
|
|
|
101
176
|
const timestamp = Date.now().toString();
|
|
@@ -108,17 +183,18 @@ export const add = new Command()
|
|
|
108
183
|
].join("\n");
|
|
109
184
|
|
|
110
185
|
// --------------------------------------------------
|
|
111
|
-
//
|
|
186
|
+
// 4. Sign request with secure device private key
|
|
112
187
|
// --------------------------------------------------
|
|
113
188
|
|
|
114
189
|
const signature = await signWithDeviceKey(
|
|
115
190
|
new TextEncoder().encode(message),
|
|
116
191
|
);
|
|
117
192
|
|
|
118
|
-
const signatureBase64 =
|
|
193
|
+
const signatureBase64 =
|
|
194
|
+
Buffer.from(signature).toString("base64");
|
|
119
195
|
|
|
120
196
|
// --------------------------------------------------
|
|
121
|
-
//
|
|
197
|
+
// 5. Request component from registry
|
|
122
198
|
// --------------------------------------------------
|
|
123
199
|
|
|
124
200
|
const response = await fetch(
|
|
@@ -138,23 +214,38 @@ export const add = new Command()
|
|
|
138
214
|
},
|
|
139
215
|
);
|
|
140
216
|
|
|
217
|
+
// --------------------------------------------------
|
|
218
|
+
// 6. Handle registry errors
|
|
219
|
+
// --------------------------------------------------
|
|
220
|
+
|
|
141
221
|
if (!response.ok) {
|
|
142
|
-
const errorData =
|
|
222
|
+
const errorData =
|
|
223
|
+
await response.json().catch(() => null);
|
|
143
224
|
|
|
144
|
-
s.stop(
|
|
225
|
+
s.stop(
|
|
226
|
+
pc.red("Component fetch failed."),
|
|
227
|
+
);
|
|
145
228
|
|
|
146
|
-
cancel(
|
|
229
|
+
cancel(
|
|
230
|
+
errorData?.error ||
|
|
231
|
+
`Server returned HTTP ${response.status}`,
|
|
232
|
+
);
|
|
147
233
|
|
|
148
234
|
process.exit(1);
|
|
149
235
|
}
|
|
150
236
|
|
|
151
|
-
const componentData =
|
|
237
|
+
const componentData =
|
|
238
|
+
await response.json();
|
|
152
239
|
|
|
153
240
|
// --------------------------------------------------
|
|
154
|
-
//
|
|
241
|
+
// 7. Create component directory
|
|
155
242
|
// --------------------------------------------------
|
|
156
243
|
|
|
157
|
-
const targetDir = path.join(
|
|
244
|
+
const targetDir = path.join(
|
|
245
|
+
cwd,
|
|
246
|
+
config.componentsPath,
|
|
247
|
+
componentName,
|
|
248
|
+
);
|
|
158
249
|
|
|
159
250
|
if (!fs.existsSync(targetDir)) {
|
|
160
251
|
fs.mkdirSync(targetDir, {
|
|
@@ -163,32 +254,48 @@ export const add = new Command()
|
|
|
163
254
|
}
|
|
164
255
|
|
|
165
256
|
// --------------------------------------------------
|
|
166
|
-
//
|
|
257
|
+
// 8. Extract bundled file names
|
|
167
258
|
// --------------------------------------------------
|
|
168
259
|
|
|
169
|
-
const bundledFileNames =
|
|
170
|
-
|
|
171
|
-
|
|
260
|
+
const bundledFileNames =
|
|
261
|
+
componentData.files.map(
|
|
262
|
+
(file: any) =>
|
|
263
|
+
file.name.replace(
|
|
264
|
+
/\.[^/.]+$/,
|
|
265
|
+
"",
|
|
266
|
+
),
|
|
267
|
+
);
|
|
172
268
|
|
|
173
269
|
// --------------------------------------------------
|
|
174
|
-
//
|
|
270
|
+
// 9. Write component files
|
|
175
271
|
// --------------------------------------------------
|
|
176
272
|
|
|
177
273
|
for (const file of componentData.files) {
|
|
178
|
-
const filePath = path.join(
|
|
274
|
+
const filePath = path.join(
|
|
275
|
+
targetDir,
|
|
276
|
+
file.name,
|
|
277
|
+
);
|
|
179
278
|
|
|
180
279
|
let content = file.content;
|
|
181
280
|
|
|
182
|
-
bundledFileNames.forEach(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
281
|
+
bundledFileNames.forEach(
|
|
282
|
+
(fileName: string) => {
|
|
283
|
+
const regex = new RegExp(
|
|
284
|
+
`from\\s+["']\\.[^"']*?\\/${fileName}["']`,
|
|
285
|
+
"g",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
content = content.replace(
|
|
289
|
+
regex,
|
|
290
|
+
`from "./${fileName}"`,
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
);
|
|
190
294
|
|
|
191
|
-
fs.writeFileSync(
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
filePath,
|
|
297
|
+
content,
|
|
298
|
+
);
|
|
192
299
|
}
|
|
193
300
|
|
|
194
301
|
s.stop(
|
|
@@ -200,43 +307,84 @@ export const add = new Command()
|
|
|
200
307
|
);
|
|
201
308
|
|
|
202
309
|
// --------------------------------------------------
|
|
203
|
-
//
|
|
310
|
+
// 10. Install ONLY missing dependencies
|
|
204
311
|
// --------------------------------------------------
|
|
205
312
|
|
|
206
|
-
|
|
207
|
-
|
|
313
|
+
const registryDependencies =
|
|
314
|
+
Array.isArray(componentData.dependencies)
|
|
315
|
+
? componentData.dependencies
|
|
316
|
+
: [];
|
|
317
|
+
|
|
318
|
+
const missingDependencies =
|
|
319
|
+
getMissingDependencies(
|
|
320
|
+
registryDependencies,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (missingDependencies.length > 0) {
|
|
324
|
+
const depsToInstall =
|
|
325
|
+
missingDependencies.join(" ");
|
|
208
326
|
|
|
209
327
|
s.start(
|
|
210
|
-
`Installing missing dependencies: ${pc.cyan(
|
|
328
|
+
`Installing missing dependencies: ${pc.cyan(
|
|
329
|
+
depsToInstall,
|
|
330
|
+
)}...`,
|
|
211
331
|
);
|
|
212
332
|
|
|
213
333
|
try {
|
|
214
|
-
execSync(
|
|
215
|
-
|
|
216
|
-
|
|
334
|
+
execSync(
|
|
335
|
+
`npm install ${depsToInstall}`,
|
|
336
|
+
{
|
|
337
|
+
stdio: "ignore",
|
|
338
|
+
},
|
|
339
|
+
);
|
|
217
340
|
|
|
218
341
|
s.stop(
|
|
219
342
|
pc.green(
|
|
220
|
-
`Dependencies installed successfully: ${pc.gray(
|
|
343
|
+
`Dependencies installed successfully: ${pc.gray(
|
|
344
|
+
depsToInstall,
|
|
345
|
+
)}`,
|
|
221
346
|
),
|
|
222
347
|
);
|
|
223
348
|
} catch {
|
|
224
|
-
s.stop(
|
|
349
|
+
s.stop(
|
|
350
|
+
pc.red(
|
|
351
|
+
"Failed to auto-install dependencies.",
|
|
352
|
+
),
|
|
353
|
+
);
|
|
225
354
|
|
|
226
355
|
note(
|
|
227
|
-
`Please run: ${pc.cyan(
|
|
356
|
+
`Please run: ${pc.cyan(
|
|
357
|
+
`npm install ${depsToInstall}`,
|
|
358
|
+
)} manually.`,
|
|
228
359
|
"Manual Action Required",
|
|
229
360
|
);
|
|
230
361
|
}
|
|
231
362
|
}
|
|
232
363
|
|
|
233
364
|
// --------------------------------------------------
|
|
234
|
-
//
|
|
365
|
+
// 11. No dependencies needed
|
|
366
|
+
// --------------------------------------------------
|
|
367
|
+
|
|
368
|
+
// We intentionally don't show a spinner here.
|
|
369
|
+
//
|
|
370
|
+
// Example:
|
|
371
|
+
//
|
|
372
|
+
// DS01 requires ["motion"]
|
|
373
|
+
// package.json already contains "motion"
|
|
374
|
+
//
|
|
375
|
+
// → Nothing happens.
|
|
376
|
+
// → No npm install.
|
|
377
|
+
// → No unnecessary package changes.
|
|
378
|
+
|
|
379
|
+
// --------------------------------------------------
|
|
380
|
+
// 12. Success
|
|
235
381
|
// --------------------------------------------------
|
|
236
382
|
|
|
237
383
|
const mainFile =
|
|
238
384
|
bundledFileNames.find(
|
|
239
|
-
(name: string) =>
|
|
385
|
+
(name: string) =>
|
|
386
|
+
name.toLowerCase() ===
|
|
387
|
+
componentName.toLowerCase(),
|
|
240
388
|
) || bundledFileNames[0];
|
|
241
389
|
|
|
242
390
|
outro(
|
|
@@ -249,10 +397,19 @@ export const add = new Command()
|
|
|
249
397
|
),
|
|
250
398
|
);
|
|
251
399
|
} catch (error: any) {
|
|
252
|
-
s.stop(
|
|
400
|
+
s.stop(
|
|
401
|
+
pc.red(
|
|
402
|
+
"An error occurred during injection.",
|
|
403
|
+
),
|
|
404
|
+
);
|
|
253
405
|
|
|
254
|
-
cancel(
|
|
406
|
+
cancel(
|
|
407
|
+
pc.gray(
|
|
408
|
+
error?.message ||
|
|
409
|
+
"Unknown CLI Error",
|
|
410
|
+
),
|
|
411
|
+
);
|
|
255
412
|
|
|
256
413
|
process.exit(1);
|
|
257
414
|
}
|
|
258
|
-
});
|
|
415
|
+
});
|
|
@@ -14,8 +14,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
14
14
|
const __dirname = path.dirname(__filename);
|
|
15
15
|
|
|
16
16
|
function getMacHelperPath(): string {
|
|
17
|
-
// dist/utils/secureKeyStore.js
|
|
18
|
-
// ../native/macos-key-helper.swift
|
|
19
17
|
return path.resolve(
|
|
20
18
|
__dirname,
|
|
21
19
|
"../native/macos-key-helper.swift",
|
|
@@ -36,54 +34,17 @@ function ensureSupportedOS(): void {
|
|
|
36
34
|
* ---------------------------------------------------------
|
|
37
35
|
* WINDOWS
|
|
38
36
|
* ---------------------------------------------------------
|
|
39
|
-
*
|
|
40
|
-
* Windows uses:
|
|
41
|
-
*
|
|
42
|
-
* Microsoft Platform Crypto Provider
|
|
43
|
-
* RSA 2048
|
|
44
|
-
* NonExportable private key
|
|
45
|
-
*
|
|
46
|
-
* The private key never enters Node.js.
|
|
47
|
-
*
|
|
48
|
-
* Node -> PowerShell -> Windows crypto provider
|
|
49
|
-
* ↓
|
|
50
|
-
* private key
|
|
51
|
-
* ↓
|
|
52
|
-
* signature
|
|
53
|
-
* ↓
|
|
54
|
-
* Node
|
|
55
|
-
*
|
|
56
|
-
* IMPORTANT:
|
|
57
|
-
*
|
|
58
|
-
* We identify the certificate using its properties rather
|
|
59
|
-
* than blindly taking the first certificate with the same
|
|
60
|
-
* subject.
|
|
61
37
|
*/
|
|
62
38
|
|
|
63
|
-
/**
|
|
64
|
-
* PowerShell expression used to identify a valid DS01
|
|
65
|
-
* certificate.
|
|
66
|
-
*
|
|
67
|
-
* Requirements:
|
|
68
|
-
*
|
|
69
|
-
* - Correct subject
|
|
70
|
-
* - Has private key
|
|
71
|
-
* - RSA certificate
|
|
72
|
-
* - Platform Crypto Provider
|
|
73
|
-
*/
|
|
74
|
-
const WINDOWS_CERT_FILTER = `
|
|
75
|
-
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
76
|
-
$_.HasPrivateKey -and
|
|
77
|
-
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
78
|
-
`;
|
|
79
|
-
|
|
80
39
|
async function createWindowsDeviceKey(): Promise<void> {
|
|
81
40
|
const script = `
|
|
82
41
|
$ErrorActionPreference = "Stop"
|
|
83
42
|
|
|
84
43
|
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
85
44
|
Where-Object {
|
|
86
|
-
${
|
|
45
|
+
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
46
|
+
$_.HasPrivateKey -and
|
|
47
|
+
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
87
48
|
})
|
|
88
49
|
|
|
89
50
|
if ($certificates.Count -gt 1) {
|
|
@@ -97,7 +58,6 @@ if ($certificates.Count -eq 1) {
|
|
|
97
58
|
|
|
98
59
|
$params = @{
|
|
99
60
|
Type = "Custom"
|
|
100
|
-
|
|
101
61
|
Subject = "CN=${KEY_NAME}"
|
|
102
62
|
|
|
103
63
|
Provider = "Microsoft Platform Crypto Provider"
|
|
@@ -105,7 +65,6 @@ $params = @{
|
|
|
105
65
|
KeyAlgorithm = "RSA"
|
|
106
66
|
KeyLength = 2048
|
|
107
67
|
|
|
108
|
-
# Private key cannot be exported.
|
|
109
68
|
KeyExportPolicy = "NonExportable"
|
|
110
69
|
|
|
111
70
|
KeyUsage = "DigitalSignature"
|
|
@@ -150,18 +109,21 @@ Write-Output "CREATED"
|
|
|
150
109
|
}
|
|
151
110
|
|
|
152
111
|
/**
|
|
153
|
-
* Find
|
|
112
|
+
* Find exactly one valid DS01 certificate.
|
|
154
113
|
*
|
|
155
|
-
* We
|
|
114
|
+
* We intentionally do not use Select-Object -First 1.
|
|
156
115
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
116
|
+
* 0 certificates -> error
|
|
117
|
+
* 1 certificate -> use it
|
|
118
|
+
* 2+ certificates -> error
|
|
159
119
|
*/
|
|
160
|
-
function
|
|
120
|
+
function getWindowsCertificateLookup(): string {
|
|
161
121
|
return `
|
|
162
122
|
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
163
123
|
Where-Object {
|
|
164
|
-
${
|
|
124
|
+
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
125
|
+
$_.HasPrivateKey -and
|
|
126
|
+
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
165
127
|
})
|
|
166
128
|
|
|
167
129
|
if ($certificates.Count -eq 0) {
|
|
@@ -172,7 +134,7 @@ if ($certificates.Count -gt 1) {
|
|
|
172
134
|
throw "Multiple valid DS01 device certificates found."
|
|
173
135
|
}
|
|
174
136
|
|
|
175
|
-
$certificates[0]
|
|
137
|
+
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificates[0]
|
|
176
138
|
`;
|
|
177
139
|
}
|
|
178
140
|
|
|
@@ -180,12 +142,8 @@ async function getWindowsPublicKey(): Promise<string> {
|
|
|
180
142
|
const script = `
|
|
181
143
|
$ErrorActionPreference = "Stop"
|
|
182
144
|
|
|
183
|
-
$
|
|
145
|
+
${getWindowsCertificateLookup()}
|
|
184
146
|
|
|
185
|
-
# Return the complete public certificate.
|
|
186
|
-
#
|
|
187
|
-
# The certificate contains the public key and certificate
|
|
188
|
-
# metadata, but never the private key.
|
|
189
147
|
[Convert]::ToBase64String($cert.RawData)
|
|
190
148
|
`;
|
|
191
149
|
|
|
@@ -224,7 +182,7 @@ async function signWindowsDeviceKey(
|
|
|
224
182
|
const script = `
|
|
225
183
|
$ErrorActionPreference = "Stop"
|
|
226
184
|
|
|
227
|
-
$
|
|
185
|
+
${getWindowsCertificateLookup()}
|
|
228
186
|
|
|
229
187
|
if (-not $cert.HasPrivateKey) {
|
|
230
188
|
throw "DS01 device certificate has no private key."
|
|
@@ -287,7 +245,9 @@ $ErrorActionPreference = "Stop"
|
|
|
287
245
|
|
|
288
246
|
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
289
247
|
Where-Object {
|
|
290
|
-
${
|
|
248
|
+
$_.Subject -eq "CN=${KEY_NAME}" -and
|
|
249
|
+
$_.HasPrivateKey -and
|
|
250
|
+
$_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
|
|
291
251
|
})
|
|
292
252
|
|
|
293
253
|
if ($certificates.Count -gt 1) {
|