ds-01 1.0.7 → 1.0.9
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 +5 -7
- package/dist/utils/secureKeyStore.d.ts.map +1 -1
- package/dist/utils/secureKeyStore.js +102 -33
- package/package.json +1 -1
- package/src/commands/add.ts +44 -125
- package/src/utils/secureKeyStore.ts +135 -44
|
@@ -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;AAuDpC,eAAO,MAAM,GAAG,SA0MZ,CAAC"}
|
package/dist/commands/add.js
CHANGED
|
@@ -84,8 +84,9 @@ export const add = new Command()
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
if (!response.ok) {
|
|
87
|
+
const errorData = await response.json().catch(() => null);
|
|
87
88
|
s.stop(pc.red("Component fetch failed."));
|
|
88
|
-
cancel(
|
|
89
|
+
cancel(errorData?.error || `Server returned HTTP ${response.status}`);
|
|
89
90
|
process.exit(1);
|
|
90
91
|
}
|
|
91
92
|
const componentData = await response.json();
|
|
@@ -118,8 +119,7 @@ export const add = new Command()
|
|
|
118
119
|
// --------------------------------------------------
|
|
119
120
|
// 7. Install dependencies
|
|
120
121
|
// --------------------------------------------------
|
|
121
|
-
if (componentData.dependencies &&
|
|
122
|
-
componentData.dependencies.length > 0) {
|
|
122
|
+
if (componentData.dependencies && componentData.dependencies.length > 0) {
|
|
123
123
|
const depsToInstall = componentData.dependencies.join(" ");
|
|
124
124
|
s.start(`Installing missing dependencies: ${pc.cyan(depsToInstall)}...`);
|
|
125
125
|
try {
|
|
@@ -136,16 +136,14 @@ export const add = new Command()
|
|
|
136
136
|
// --------------------------------------------------
|
|
137
137
|
// 8. Success
|
|
138
138
|
// --------------------------------------------------
|
|
139
|
-
const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
|
|
140
|
-
componentName.toLowerCase()) || bundledFileNames[0];
|
|
139
|
+
const mainFile = bundledFileNames.find((name) => name.toLowerCase() === componentName.toLowerCase()) || bundledFileNames[0];
|
|
141
140
|
outro(`${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
|
|
142
141
|
pc.gray("Import it: ") +
|
|
143
142
|
pc.cyan(`import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`));
|
|
144
143
|
}
|
|
145
144
|
catch (error) {
|
|
146
145
|
s.stop(pc.red("An error occurred during injection."));
|
|
147
|
-
cancel(pc.gray(error?.message ||
|
|
148
|
-
"Unknown CLI Error"));
|
|
146
|
+
cancel(pc.gray(error?.message || "Unknown CLI Error"));
|
|
149
147
|
process.exit(1);
|
|
150
148
|
}
|
|
151
149
|
});
|
|
@@ -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":"AAmZA;;;;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"}
|
|
@@ -23,24 +23,66 @@ function ensureSupportedOS() {
|
|
|
23
23
|
* ---------------------------------------------------------
|
|
24
24
|
* WINDOWS
|
|
25
25
|
* ---------------------------------------------------------
|
|
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.
|
|
26
48
|
*/
|
|
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
|
+
`;
|
|
27
65
|
async function createWindowsDeviceKey() {
|
|
28
66
|
const script = `
|
|
29
67
|
$ErrorActionPreference = "Stop"
|
|
30
68
|
|
|
31
|
-
$
|
|
69
|
+
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
32
70
|
Where-Object {
|
|
33
|
-
$
|
|
34
|
-
}
|
|
35
|
-
|
|
71
|
+
${WINDOWS_CERT_FILTER}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if ($certificates.Count -gt 1) {
|
|
75
|
+
throw "Multiple valid DS01 device certificates found. Remove old DS01 device certificates before continuing."
|
|
76
|
+
}
|
|
36
77
|
|
|
37
|
-
if ($
|
|
78
|
+
if ($certificates.Count -eq 1) {
|
|
38
79
|
Write-Output "EXISTS"
|
|
39
80
|
exit 0
|
|
40
81
|
}
|
|
41
82
|
|
|
42
83
|
$params = @{
|
|
43
84
|
Type = "Custom"
|
|
85
|
+
|
|
44
86
|
Subject = "CN=${KEY_NAME}"
|
|
45
87
|
|
|
46
88
|
Provider = "Microsoft Platform Crypto Provider"
|
|
@@ -48,6 +90,7 @@ $params = @{
|
|
|
48
90
|
KeyAlgorithm = "RSA"
|
|
49
91
|
KeyLength = 2048
|
|
50
92
|
|
|
93
|
+
# Private key cannot be exported.
|
|
51
94
|
KeyExportPolicy = "NonExportable"
|
|
52
95
|
|
|
53
96
|
KeyUsage = "DigitalSignature"
|
|
@@ -58,7 +101,11 @@ $params = @{
|
|
|
58
101
|
NotAfter = (Get-Date).AddYears(10)
|
|
59
102
|
}
|
|
60
103
|
|
|
61
|
-
New-SelfSignedCertificate @params
|
|
104
|
+
$certificate = New-SelfSignedCertificate @params
|
|
105
|
+
|
|
106
|
+
if (-not $certificate) {
|
|
107
|
+
throw "Windows failed to create the DS01 device certificate."
|
|
108
|
+
}
|
|
62
109
|
|
|
63
110
|
Write-Output "CREATED"
|
|
64
111
|
`;
|
|
@@ -77,20 +124,42 @@ Write-Output "CREATED"
|
|
|
77
124
|
throw new Error("Failed to create Windows DS01 device key.");
|
|
78
125
|
}
|
|
79
126
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Find the exact DS01 certificate.
|
|
129
|
+
*
|
|
130
|
+
* We deliberately DO NOT use Select-Object -First 1.
|
|
131
|
+
*
|
|
132
|
+
* If multiple matching certificates exist, we fail instead
|
|
133
|
+
* of potentially using the wrong private key.
|
|
134
|
+
*/
|
|
135
|
+
function windowsCertificateLookupScript() {
|
|
136
|
+
return `
|
|
137
|
+
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
85
138
|
Where-Object {
|
|
86
|
-
$
|
|
87
|
-
}
|
|
88
|
-
Select-Object -First 1
|
|
139
|
+
${WINDOWS_CERT_FILTER}
|
|
140
|
+
})
|
|
89
141
|
|
|
90
|
-
if (-
|
|
142
|
+
if ($certificates.Count -eq 0) {
|
|
91
143
|
throw "DS01 device key not found."
|
|
92
144
|
}
|
|
93
145
|
|
|
146
|
+
if ($certificates.Count -gt 1) {
|
|
147
|
+
throw "Multiple valid DS01 device certificates found."
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
$certificates[0]
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
async function getWindowsPublicKey() {
|
|
154
|
+
const script = `
|
|
155
|
+
$ErrorActionPreference = "Stop"
|
|
156
|
+
|
|
157
|
+
$cert = ${windowsCertificateLookupScript()}
|
|
158
|
+
|
|
159
|
+
# Return the complete public certificate.
|
|
160
|
+
#
|
|
161
|
+
# The certificate contains the public key and certificate
|
|
162
|
+
# metadata, but never the private key.
|
|
94
163
|
[Convert]::ToBase64String($cert.RawData)
|
|
95
164
|
`;
|
|
96
165
|
const { stdout } = await execFileAsync("powershell.exe", [
|
|
@@ -114,21 +183,14 @@ async function signWindowsDeviceKey(data) {
|
|
|
114
183
|
const script = `
|
|
115
184
|
$ErrorActionPreference = "Stop"
|
|
116
185
|
|
|
117
|
-
$cert =
|
|
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
|
-
}
|
|
186
|
+
$cert = ${windowsCertificateLookupScript()}
|
|
126
187
|
|
|
127
188
|
if (-not $cert.HasPrivateKey) {
|
|
128
189
|
throw "DS01 device certificate has no private key."
|
|
129
190
|
}
|
|
130
191
|
|
|
131
|
-
$rsa =
|
|
192
|
+
$rsa =
|
|
193
|
+
[System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
|
|
132
194
|
|
|
133
195
|
if (-not $rsa) {
|
|
134
196
|
throw "Unable to access DS01 private signing key."
|
|
@@ -169,15 +231,20 @@ async function deleteWindowsDeviceKey() {
|
|
|
169
231
|
const script = `
|
|
170
232
|
$ErrorActionPreference = "Stop"
|
|
171
233
|
|
|
172
|
-
$
|
|
234
|
+
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
173
235
|
Where-Object {
|
|
174
|
-
$
|
|
175
|
-
}
|
|
176
|
-
Select-Object -First 1
|
|
236
|
+
${WINDOWS_CERT_FILTER}
|
|
237
|
+
})
|
|
177
238
|
|
|
178
|
-
if ($
|
|
179
|
-
|
|
239
|
+
if ($certificates.Count -gt 1) {
|
|
240
|
+
throw "Multiple valid DS01 device certificates found."
|
|
180
241
|
}
|
|
242
|
+
|
|
243
|
+
if ($certificates.Count -eq 1) {
|
|
244
|
+
Remove-Item $certificates[0].PSPath
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Write-Output "DELETED"
|
|
181
248
|
`;
|
|
182
249
|
await execFileAsync("powershell.exe", [
|
|
183
250
|
"-NoProfile",
|
|
@@ -208,8 +275,10 @@ async function runMacHelper(operation, data) {
|
|
|
208
275
|
}
|
|
209
276
|
async function createMacDeviceKey() {
|
|
210
277
|
const result = await runMacHelper("create");
|
|
211
|
-
if (result !== "CREATED" &&
|
|
212
|
-
|
|
278
|
+
if (result !== "CREATED" &&
|
|
279
|
+
result !== "EXISTS") {
|
|
280
|
+
throw new Error(result ||
|
|
281
|
+
"Failed to create macOS Secure Enclave device key.");
|
|
213
282
|
}
|
|
214
283
|
}
|
|
215
284
|
async function getMacPublicKey() {
|
package/package.json
CHANGED
package/src/commands/add.ts
CHANGED
|
@@ -12,8 +12,7 @@ import machineIdPkg from "node-machine-id";
|
|
|
12
12
|
|
|
13
13
|
const { machineIdSync } = machineIdPkg;
|
|
14
14
|
|
|
15
|
-
const API_BASE_URL =
|
|
16
|
-
process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
15
|
+
const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
17
16
|
|
|
18
17
|
// Helper to verify Tailwind exists before doing anything
|
|
19
18
|
function checkTailwindInstallation() {
|
|
@@ -29,9 +28,7 @@ function checkTailwindInstallation() {
|
|
|
29
28
|
process.exit(1);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
const pkgJson = JSON.parse(
|
|
33
|
-
fs.readFileSync(pkgJsonPath, "utf-8"),
|
|
34
|
-
);
|
|
31
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
35
32
|
|
|
36
33
|
const allDeps = {
|
|
37
34
|
...pkgJson.dependencies,
|
|
@@ -40,22 +37,16 @@ function checkTailwindInstallation() {
|
|
|
40
37
|
|
|
41
38
|
if (!allDeps["tailwindcss"]) {
|
|
42
39
|
cancel(
|
|
43
|
-
pc.red(
|
|
44
|
-
"Tailwind CSS is missing from your project dependencies.\n",
|
|
45
|
-
) +
|
|
40
|
+
pc.red("Tailwind CSS is missing from your project dependencies.\n") +
|
|
46
41
|
pc.gray(
|
|
47
42
|
"DS01 components rely strictly on Tailwind CSS for styling.\n\n",
|
|
48
43
|
) +
|
|
49
44
|
pc.white(
|
|
50
45
|
"Kindly install it and configure your project, then retry:\n",
|
|
51
46
|
) +
|
|
52
|
-
pc.cyan(
|
|
53
|
-
" npm install tailwindcss @tailwindcss/postcss postcss\n\n",
|
|
54
|
-
) +
|
|
47
|
+
pc.cyan(" npm install tailwindcss @tailwindcss/postcss postcss\n\n") +
|
|
55
48
|
pc.gray("Official Setup Guide: ") +
|
|
56
|
-
pc.underline(
|
|
57
|
-
"https://tailwindcss.com/docs/installation",
|
|
58
|
-
),
|
|
49
|
+
pc.underline("https://tailwindcss.com/docs/installation"),
|
|
59
50
|
);
|
|
60
51
|
|
|
61
52
|
process.exit(1);
|
|
@@ -65,10 +56,7 @@ function checkTailwindInstallation() {
|
|
|
65
56
|
export const add = new Command()
|
|
66
57
|
.name("add")
|
|
67
58
|
.description("Add a component from DS01 to your project")
|
|
68
|
-
.argument(
|
|
69
|
-
"<component>",
|
|
70
|
-
"The name of the component (e.g., premium-section)",
|
|
71
|
-
)
|
|
59
|
+
.argument("<component>", "The name of the component (e.g., premium-section)")
|
|
72
60
|
.action(async (componentName: string) => {
|
|
73
61
|
console.log();
|
|
74
62
|
|
|
@@ -82,45 +70,28 @@ export const add = new Command()
|
|
|
82
70
|
|
|
83
71
|
const cwd = process.cwd();
|
|
84
72
|
|
|
85
|
-
const configPath = path.join(
|
|
86
|
-
cwd,
|
|
87
|
-
"ds01.config.json",
|
|
88
|
-
);
|
|
73
|
+
const configPath = path.join(cwd, "ds01.config.json");
|
|
89
74
|
|
|
90
75
|
if (!fs.existsSync(configPath)) {
|
|
91
|
-
cancel(
|
|
92
|
-
pc.red(
|
|
93
|
-
"ds01.config.json not found. Run 'npx ds-01 init' first.",
|
|
94
|
-
),
|
|
95
|
-
);
|
|
76
|
+
cancel(pc.red("ds01.config.json not found. Run 'npx ds-01 init' first."));
|
|
96
77
|
|
|
97
78
|
process.exit(1);
|
|
98
79
|
}
|
|
99
80
|
|
|
100
|
-
const config = JSON.parse(
|
|
101
|
-
fs.readFileSync(configPath, "utf-8"),
|
|
102
|
-
);
|
|
81
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
103
82
|
|
|
104
83
|
const token = getToken();
|
|
105
84
|
const machineId = machineIdSync();
|
|
106
85
|
|
|
107
86
|
if (!token || !machineId) {
|
|
108
|
-
cancel(
|
|
109
|
-
pc.red(
|
|
110
|
-
"You are not authenticated. Run 'npx ds-01 login' first.",
|
|
111
|
-
),
|
|
112
|
-
);
|
|
87
|
+
cancel(pc.red("You are not authenticated. Run 'npx ds-01 login' first."));
|
|
113
88
|
|
|
114
89
|
process.exit(1);
|
|
115
90
|
}
|
|
116
91
|
|
|
117
92
|
const s = spinner();
|
|
118
93
|
|
|
119
|
-
s.start(
|
|
120
|
-
`Fetching ${pc.cyan(
|
|
121
|
-
`<${componentName} />`,
|
|
122
|
-
)} from registry...`,
|
|
123
|
-
);
|
|
94
|
+
s.start(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
|
|
124
95
|
|
|
125
96
|
try {
|
|
126
97
|
// --------------------------------------------------
|
|
@@ -144,8 +115,7 @@ export const add = new Command()
|
|
|
144
115
|
new TextEncoder().encode(message),
|
|
145
116
|
);
|
|
146
117
|
|
|
147
|
-
const signatureBase64 =
|
|
148
|
-
Buffer.from(signature).toString("base64");
|
|
118
|
+
const signatureBase64 = Buffer.from(signature).toString("base64");
|
|
149
119
|
|
|
150
120
|
// --------------------------------------------------
|
|
151
121
|
// 3. Send token + machine ID + signature
|
|
@@ -169,29 +139,22 @@ export const add = new Command()
|
|
|
169
139
|
);
|
|
170
140
|
|
|
171
141
|
if (!response.ok) {
|
|
172
|
-
|
|
173
|
-
pc.red("Component fetch failed."),
|
|
174
|
-
);
|
|
142
|
+
const errorData = await response.json().catch(() => null);
|
|
175
143
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
);
|
|
144
|
+
s.stop(pc.red("Component fetch failed."));
|
|
145
|
+
|
|
146
|
+
cancel(errorData?.error || `Server returned HTTP ${response.status}`);
|
|
179
147
|
|
|
180
148
|
process.exit(1);
|
|
181
149
|
}
|
|
182
150
|
|
|
183
|
-
const componentData =
|
|
184
|
-
await response.json();
|
|
151
|
+
const componentData = await response.json();
|
|
185
152
|
|
|
186
153
|
// --------------------------------------------------
|
|
187
154
|
// 4. Create component folder
|
|
188
155
|
// --------------------------------------------------
|
|
189
156
|
|
|
190
|
-
const targetDir = path.join(
|
|
191
|
-
cwd,
|
|
192
|
-
config.componentsPath,
|
|
193
|
-
componentName,
|
|
194
|
-
);
|
|
157
|
+
const targetDir = path.join(cwd, config.componentsPath, componentName);
|
|
195
158
|
|
|
196
159
|
if (!fs.existsSync(targetDir)) {
|
|
197
160
|
fs.mkdirSync(targetDir, {
|
|
@@ -203,45 +166,29 @@ export const add = new Command()
|
|
|
203
166
|
// 5. Extract bundled file names
|
|
204
167
|
// --------------------------------------------------
|
|
205
168
|
|
|
206
|
-
const bundledFileNames =
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
file.name.replace(
|
|
210
|
-
/\.[^/.]+$/,
|
|
211
|
-
"",
|
|
212
|
-
),
|
|
213
|
-
);
|
|
169
|
+
const bundledFileNames = componentData.files.map((file: any) =>
|
|
170
|
+
file.name.replace(/\.[^/.]+$/, ""),
|
|
171
|
+
);
|
|
214
172
|
|
|
215
173
|
// --------------------------------------------------
|
|
216
174
|
// 6. Write component files
|
|
217
175
|
// --------------------------------------------------
|
|
218
176
|
|
|
219
177
|
for (const file of componentData.files) {
|
|
220
|
-
const filePath = path.join(
|
|
221
|
-
targetDir,
|
|
222
|
-
file.name,
|
|
223
|
-
);
|
|
178
|
+
const filePath = path.join(targetDir, file.name);
|
|
224
179
|
|
|
225
180
|
let content = file.content;
|
|
226
181
|
|
|
227
|
-
bundledFileNames.forEach(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
content = content.replace(
|
|
235
|
-
regex,
|
|
236
|
-
`from "./${fileName}"`,
|
|
237
|
-
);
|
|
238
|
-
},
|
|
239
|
-
);
|
|
182
|
+
bundledFileNames.forEach((fileName: string) => {
|
|
183
|
+
const regex = new RegExp(
|
|
184
|
+
`from\\s+["']\\.[^"']*?\\/${fileName}["']`,
|
|
185
|
+
"g",
|
|
186
|
+
);
|
|
240
187
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
188
|
+
content = content.replace(regex, `from "./${fileName}"`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
fs.writeFileSync(filePath, content);
|
|
245
192
|
}
|
|
246
193
|
|
|
247
194
|
s.stop(
|
|
@@ -256,45 +203,28 @@ export const add = new Command()
|
|
|
256
203
|
// 7. Install dependencies
|
|
257
204
|
// --------------------------------------------------
|
|
258
205
|
|
|
259
|
-
if (
|
|
260
|
-
componentData.dependencies
|
|
261
|
-
componentData.dependencies.length > 0
|
|
262
|
-
) {
|
|
263
|
-
const depsToInstall =
|
|
264
|
-
componentData.dependencies.join(" ");
|
|
206
|
+
if (componentData.dependencies && componentData.dependencies.length > 0) {
|
|
207
|
+
const depsToInstall = componentData.dependencies.join(" ");
|
|
265
208
|
|
|
266
209
|
s.start(
|
|
267
|
-
`Installing missing dependencies: ${pc.cyan(
|
|
268
|
-
depsToInstall,
|
|
269
|
-
)}...`,
|
|
210
|
+
`Installing missing dependencies: ${pc.cyan(depsToInstall)}...`,
|
|
270
211
|
);
|
|
271
212
|
|
|
272
213
|
try {
|
|
273
|
-
execSync(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
stdio: "ignore",
|
|
277
|
-
},
|
|
278
|
-
);
|
|
214
|
+
execSync(`npm install ${depsToInstall}`, {
|
|
215
|
+
stdio: "ignore",
|
|
216
|
+
});
|
|
279
217
|
|
|
280
218
|
s.stop(
|
|
281
219
|
pc.green(
|
|
282
|
-
`Dependencies installed successfully: ${pc.gray(
|
|
283
|
-
depsToInstall,
|
|
284
|
-
)}`,
|
|
220
|
+
`Dependencies installed successfully: ${pc.gray(depsToInstall)}`,
|
|
285
221
|
),
|
|
286
222
|
);
|
|
287
223
|
} catch {
|
|
288
|
-
s.stop(
|
|
289
|
-
pc.red(
|
|
290
|
-
"Failed to auto-install dependencies.",
|
|
291
|
-
),
|
|
292
|
-
);
|
|
224
|
+
s.stop(pc.red("Failed to auto-install dependencies."));
|
|
293
225
|
|
|
294
226
|
note(
|
|
295
|
-
`Please run: ${pc.cyan(
|
|
296
|
-
`npm install ${depsToInstall}`,
|
|
297
|
-
)} manually.`,
|
|
227
|
+
`Please run: ${pc.cyan(`npm install ${depsToInstall}`)} manually.`,
|
|
298
228
|
"Manual Action Required",
|
|
299
229
|
);
|
|
300
230
|
}
|
|
@@ -306,9 +236,7 @@ export const add = new Command()
|
|
|
306
236
|
|
|
307
237
|
const mainFile =
|
|
308
238
|
bundledFileNames.find(
|
|
309
|
-
(name: string) =>
|
|
310
|
-
name.toLowerCase() ===
|
|
311
|
-
componentName.toLowerCase(),
|
|
239
|
+
(name: string) => name.toLowerCase() === componentName.toLowerCase(),
|
|
312
240
|
) || bundledFileNames[0];
|
|
313
241
|
|
|
314
242
|
outro(
|
|
@@ -321,19 +249,10 @@ export const add = new Command()
|
|
|
321
249
|
),
|
|
322
250
|
);
|
|
323
251
|
} catch (error: any) {
|
|
324
|
-
s.stop(
|
|
325
|
-
pc.red(
|
|
326
|
-
"An error occurred during injection.",
|
|
327
|
-
),
|
|
328
|
-
);
|
|
252
|
+
s.stop(pc.red("An error occurred during injection."));
|
|
329
253
|
|
|
330
|
-
cancel(
|
|
331
|
-
pc.gray(
|
|
332
|
-
error?.message ||
|
|
333
|
-
"Unknown CLI Error",
|
|
334
|
-
),
|
|
335
|
-
);
|
|
254
|
+
cancel(pc.gray(error?.message || "Unknown CLI Error"));
|
|
336
255
|
|
|
337
256
|
process.exit(1);
|
|
338
257
|
}
|
|
339
|
-
});
|
|
258
|
+
});
|
|
@@ -22,7 +22,7 @@ function getMacHelperPath(): string {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function ensureSupportedOS() {
|
|
25
|
+
function ensureSupportedOS(): void {
|
|
26
26
|
const platform = os.platform();
|
|
27
27
|
|
|
28
28
|
if (platform !== "win32" && platform !== "darwin") {
|
|
@@ -36,25 +36,68 @@ function ensureSupportedOS() {
|
|
|
36
36
|
* ---------------------------------------------------------
|
|
37
37
|
* WINDOWS
|
|
38
38
|
* ---------------------------------------------------------
|
|
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.
|
|
39
61
|
*/
|
|
40
62
|
|
|
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
|
+
|
|
41
80
|
async function createWindowsDeviceKey(): Promise<void> {
|
|
42
81
|
const script = `
|
|
43
82
|
$ErrorActionPreference = "Stop"
|
|
44
83
|
|
|
45
|
-
$
|
|
84
|
+
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
46
85
|
Where-Object {
|
|
47
|
-
$
|
|
48
|
-
}
|
|
49
|
-
|
|
86
|
+
${WINDOWS_CERT_FILTER}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if ($certificates.Count -gt 1) {
|
|
90
|
+
throw "Multiple valid DS01 device certificates found. Remove old DS01 device certificates before continuing."
|
|
91
|
+
}
|
|
50
92
|
|
|
51
|
-
if ($
|
|
93
|
+
if ($certificates.Count -eq 1) {
|
|
52
94
|
Write-Output "EXISTS"
|
|
53
95
|
exit 0
|
|
54
96
|
}
|
|
55
97
|
|
|
56
98
|
$params = @{
|
|
57
99
|
Type = "Custom"
|
|
100
|
+
|
|
58
101
|
Subject = "CN=${KEY_NAME}"
|
|
59
102
|
|
|
60
103
|
Provider = "Microsoft Platform Crypto Provider"
|
|
@@ -62,6 +105,7 @@ $params = @{
|
|
|
62
105
|
KeyAlgorithm = "RSA"
|
|
63
106
|
KeyLength = 2048
|
|
64
107
|
|
|
108
|
+
# Private key cannot be exported.
|
|
65
109
|
KeyExportPolicy = "NonExportable"
|
|
66
110
|
|
|
67
111
|
KeyUsage = "DigitalSignature"
|
|
@@ -72,7 +116,11 @@ $params = @{
|
|
|
72
116
|
NotAfter = (Get-Date).AddYears(10)
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
New-SelfSignedCertificate @params
|
|
119
|
+
$certificate = New-SelfSignedCertificate @params
|
|
120
|
+
|
|
121
|
+
if (-not $certificate) {
|
|
122
|
+
throw "Windows failed to create the DS01 device certificate."
|
|
123
|
+
}
|
|
76
124
|
|
|
77
125
|
Write-Output "CREATED"
|
|
78
126
|
`;
|
|
@@ -95,24 +143,49 @@ Write-Output "CREATED"
|
|
|
95
143
|
const result = stdout.trim();
|
|
96
144
|
|
|
97
145
|
if (result !== "CREATED" && result !== "EXISTS") {
|
|
98
|
-
throw new Error(
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Failed to create Windows DS01 device key.",
|
|
148
|
+
);
|
|
99
149
|
}
|
|
100
150
|
}
|
|
101
151
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Find the exact DS01 certificate.
|
|
154
|
+
*
|
|
155
|
+
* We deliberately DO NOT use Select-Object -First 1.
|
|
156
|
+
*
|
|
157
|
+
* If multiple matching certificates exist, we fail instead
|
|
158
|
+
* of potentially using the wrong private key.
|
|
159
|
+
*/
|
|
160
|
+
function windowsCertificateLookupScript(): string {
|
|
161
|
+
return `
|
|
162
|
+
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
107
163
|
Where-Object {
|
|
108
|
-
$
|
|
109
|
-
}
|
|
110
|
-
Select-Object -First 1
|
|
164
|
+
${WINDOWS_CERT_FILTER}
|
|
165
|
+
})
|
|
111
166
|
|
|
112
|
-
if (-
|
|
167
|
+
if ($certificates.Count -eq 0) {
|
|
113
168
|
throw "DS01 device key not found."
|
|
114
169
|
}
|
|
115
170
|
|
|
171
|
+
if ($certificates.Count -gt 1) {
|
|
172
|
+
throw "Multiple valid DS01 device certificates found."
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
$certificates[0]
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function getWindowsPublicKey(): Promise<string> {
|
|
180
|
+
const script = `
|
|
181
|
+
$ErrorActionPreference = "Stop"
|
|
182
|
+
|
|
183
|
+
$cert = ${windowsCertificateLookupScript()}
|
|
184
|
+
|
|
185
|
+
# Return the complete public certificate.
|
|
186
|
+
#
|
|
187
|
+
# The certificate contains the public key and certificate
|
|
188
|
+
# metadata, but never the private key.
|
|
116
189
|
[Convert]::ToBase64String($cert.RawData)
|
|
117
190
|
`;
|
|
118
191
|
|
|
@@ -134,7 +207,9 @@ if (-not $cert) {
|
|
|
134
207
|
const certificate = stdout.trim();
|
|
135
208
|
|
|
136
209
|
if (!certificate) {
|
|
137
|
-
throw new Error(
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Unable to read Windows DS01 public certificate.",
|
|
212
|
+
);
|
|
138
213
|
}
|
|
139
214
|
|
|
140
215
|
return certificate;
|
|
@@ -143,26 +218,20 @@ if (-not $cert) {
|
|
|
143
218
|
async function signWindowsDeviceKey(
|
|
144
219
|
data: Uint8Array,
|
|
145
220
|
): Promise<Uint8Array> {
|
|
146
|
-
const dataBase64 =
|
|
221
|
+
const dataBase64 =
|
|
222
|
+
Buffer.from(data).toString("base64");
|
|
147
223
|
|
|
148
224
|
const script = `
|
|
149
225
|
$ErrorActionPreference = "Stop"
|
|
150
226
|
|
|
151
|
-
$cert =
|
|
152
|
-
Where-Object {
|
|
153
|
-
$_.Subject -eq "CN=${KEY_NAME}"
|
|
154
|
-
} |
|
|
155
|
-
Select-Object -First 1
|
|
156
|
-
|
|
157
|
-
if (-not $cert) {
|
|
158
|
-
throw "DS01 device key not found."
|
|
159
|
-
}
|
|
227
|
+
$cert = ${windowsCertificateLookupScript()}
|
|
160
228
|
|
|
161
229
|
if (-not $cert.HasPrivateKey) {
|
|
162
230
|
throw "DS01 device certificate has no private key."
|
|
163
231
|
}
|
|
164
232
|
|
|
165
|
-
$rsa =
|
|
233
|
+
$rsa =
|
|
234
|
+
[System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
|
|
166
235
|
|
|
167
236
|
if (-not $rsa) {
|
|
168
237
|
throw "Unable to access DS01 private signing key."
|
|
@@ -202,25 +271,34 @@ finally {
|
|
|
202
271
|
const signature = stdout.trim();
|
|
203
272
|
|
|
204
273
|
if (!signature) {
|
|
205
|
-
throw new Error(
|
|
274
|
+
throw new Error(
|
|
275
|
+
"Failed to sign data with Windows device key.",
|
|
276
|
+
);
|
|
206
277
|
}
|
|
207
278
|
|
|
208
|
-
return new Uint8Array(
|
|
279
|
+
return new Uint8Array(
|
|
280
|
+
Buffer.from(signature, "base64"),
|
|
281
|
+
);
|
|
209
282
|
}
|
|
210
283
|
|
|
211
284
|
async function deleteWindowsDeviceKey(): Promise<void> {
|
|
212
285
|
const script = `
|
|
213
286
|
$ErrorActionPreference = "Stop"
|
|
214
287
|
|
|
215
|
-
$
|
|
288
|
+
$certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
|
|
216
289
|
Where-Object {
|
|
217
|
-
$
|
|
218
|
-
}
|
|
219
|
-
Select-Object -First 1
|
|
290
|
+
${WINDOWS_CERT_FILTER}
|
|
291
|
+
})
|
|
220
292
|
|
|
221
|
-
if ($
|
|
222
|
-
|
|
293
|
+
if ($certificates.Count -gt 1) {
|
|
294
|
+
throw "Multiple valid DS01 device certificates found."
|
|
223
295
|
}
|
|
296
|
+
|
|
297
|
+
if ($certificates.Count -eq 1) {
|
|
298
|
+
Remove-Item $certificates[0].PSPath
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Write-Output "DELETED"
|
|
224
302
|
`;
|
|
225
303
|
|
|
226
304
|
await execFileAsync(
|
|
@@ -246,7 +324,11 @@ if ($cert) {
|
|
|
246
324
|
*/
|
|
247
325
|
|
|
248
326
|
async function runMacHelper(
|
|
249
|
-
operation:
|
|
327
|
+
operation:
|
|
328
|
+
| "create"
|
|
329
|
+
| "public"
|
|
330
|
+
| "sign"
|
|
331
|
+
| "delete",
|
|
250
332
|
data?: Uint8Array,
|
|
251
333
|
): Promise<string> {
|
|
252
334
|
const helper = getMacHelperPath();
|
|
@@ -254,7 +336,9 @@ async function runMacHelper(
|
|
|
254
336
|
const args = [helper, operation];
|
|
255
337
|
|
|
256
338
|
if (data) {
|
|
257
|
-
args.push(
|
|
339
|
+
args.push(
|
|
340
|
+
Buffer.from(data).toString("base64"),
|
|
341
|
+
);
|
|
258
342
|
}
|
|
259
343
|
|
|
260
344
|
const { stdout } = await execFileAsync(
|
|
@@ -269,17 +353,23 @@ async function runMacHelper(
|
|
|
269
353
|
}
|
|
270
354
|
|
|
271
355
|
async function createMacDeviceKey(): Promise<void> {
|
|
272
|
-
const result =
|
|
356
|
+
const result =
|
|
357
|
+
await runMacHelper("create");
|
|
273
358
|
|
|
274
|
-
if (
|
|
359
|
+
if (
|
|
360
|
+
result !== "CREATED" &&
|
|
361
|
+
result !== "EXISTS"
|
|
362
|
+
) {
|
|
275
363
|
throw new Error(
|
|
276
|
-
result ||
|
|
364
|
+
result ||
|
|
365
|
+
"Failed to create macOS Secure Enclave device key.",
|
|
277
366
|
);
|
|
278
367
|
}
|
|
279
368
|
}
|
|
280
369
|
|
|
281
370
|
async function getMacPublicKey(): Promise<string> {
|
|
282
|
-
const result =
|
|
371
|
+
const result =
|
|
372
|
+
await runMacHelper("public");
|
|
283
373
|
|
|
284
374
|
if (!result) {
|
|
285
375
|
throw new Error(
|
|
@@ -293,7 +383,8 @@ async function getMacPublicKey(): Promise<string> {
|
|
|
293
383
|
async function signMacDeviceKey(
|
|
294
384
|
data: Uint8Array,
|
|
295
385
|
): Promise<Uint8Array> {
|
|
296
|
-
const result =
|
|
386
|
+
const result =
|
|
387
|
+
await runMacHelper("sign", data);
|
|
297
388
|
|
|
298
389
|
if (!result) {
|
|
299
390
|
throw new Error(
|