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.
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +65 -24
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +46 -17
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logout.js +27 -7
- package/dist/index.js +2 -0
- package/dist/test-key.d.ts +2 -0
- package/dist/test-key.d.ts.map +1 -0
- package/dist/test-key.js +7 -0
- package/dist/utils/config.d.ts +0 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +0 -13
- package/dist/utils/deviceKey.d.ts +3 -0
- package/dist/utils/deviceKey.d.ts.map +1 -0
- package/dist/utils/deviceKey.js +13 -0
- package/dist/utils/secureKeyStore.d.ts +10 -0
- package/dist/utils/secureKeyStore.d.ts.map +1 -0
- package/dist/utils/secureKeyStore.js +266 -0
- package/package.json +2 -1
- package/src/commands/add.ts +235 -59
- package/src/commands/login.ts +75 -22
- package/src/commands/logout.ts +39 -10
- package/src/index.ts +2 -0
- package/src/native/macos-key-helper.swift +206 -0
- package/src/test-key.ts +20 -0
- package/src/utils/config.ts +1 -12
- package/src/utils/deviceKey.ts +19 -0
- package/src/utils/secureKeyStore.ts +361 -0
package/src/commands/login.ts
CHANGED
|
@@ -3,64 +3,106 @@ import { intro, outro, spinner, note, cancel } from "@clack/prompts";
|
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import machineIdPkg from "node-machine-id";
|
|
6
|
+
|
|
6
7
|
import { saveToken } from "../utils/config.js";
|
|
8
|
+
import {
|
|
9
|
+
createDeviceKey,
|
|
10
|
+
getDevicePublicKey,
|
|
11
|
+
} from "../utils/secureKeyStore.js";
|
|
7
12
|
|
|
8
13
|
const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
14
|
+
|
|
9
15
|
const { machineIdSync } = machineIdPkg;
|
|
10
16
|
|
|
11
17
|
export const login = new Command()
|
|
12
18
|
.name("login")
|
|
13
19
|
.description("Authenticate your terminal with DS01 via browser")
|
|
14
20
|
.action(async () => {
|
|
15
|
-
console.log();
|
|
21
|
+
console.log();
|
|
16
22
|
|
|
17
|
-
// 1. Header banner
|
|
18
23
|
intro(
|
|
19
|
-
`${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray(
|
|
24
|
+
`${pc.bgWhite(pc.black(pc.bold(" DS01 ")))} ${pc.gray(
|
|
25
|
+
"v1.0.0 — Terminal Authentication",
|
|
26
|
+
)}`,
|
|
20
27
|
);
|
|
21
28
|
|
|
22
29
|
const s = spinner();
|
|
23
30
|
|
|
24
31
|
try {
|
|
25
|
-
//
|
|
26
|
-
|
|
32
|
+
// --------------------------------------------------
|
|
33
|
+
// 1. Create / load the device's secure key
|
|
34
|
+
// --------------------------------------------------
|
|
35
|
+
s.start("Preparing secure device identity...");
|
|
36
|
+
|
|
37
|
+
await createDeviceKey();
|
|
38
|
+
|
|
39
|
+
// Only the PUBLIC certificate/key leaves the machine.
|
|
40
|
+
const publicKey = await getDevicePublicKey();
|
|
41
|
+
|
|
42
|
+
// Existing machine fingerprint.
|
|
27
43
|
const hardwareId = machineIdSync();
|
|
28
44
|
|
|
45
|
+
// --------------------------------------------------
|
|
46
|
+
// 2. Start device authorization
|
|
47
|
+
// --------------------------------------------------
|
|
48
|
+
s.start("Requesting pairing code...");
|
|
49
|
+
|
|
29
50
|
const initResponse = await fetch(`${API_BASE_URL}/api/auth/device/code`, {
|
|
30
51
|
method: "POST",
|
|
31
|
-
headers: {
|
|
32
|
-
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
machineId: hardwareId,
|
|
57
|
+
publicKey,
|
|
58
|
+
publicKeyType:
|
|
59
|
+
process.platform === "win32" ? "windows-rsa" : "macos-ec",
|
|
60
|
+
}),
|
|
33
61
|
});
|
|
34
62
|
|
|
35
63
|
if (!initResponse.ok) {
|
|
36
64
|
s.stop(pc.red("Failed to reach DS01 authorization server."));
|
|
65
|
+
|
|
37
66
|
cancel(`Server responded with HTTP ${initResponse.status}`);
|
|
67
|
+
|
|
38
68
|
process.exit(1);
|
|
39
69
|
}
|
|
40
70
|
|
|
41
71
|
const { deviceCode, userCode, verificationUrl, interval } =
|
|
42
72
|
await initResponse.json();
|
|
43
73
|
|
|
44
|
-
|
|
74
|
+
if (!deviceCode || !userCode || !verificationUrl) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Authorization server returned an invalid device response.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
45
79
|
|
|
46
|
-
|
|
80
|
+
s.stop(pc.green("Secure device identity registered."));
|
|
81
|
+
|
|
82
|
+
// --------------------------------------------------
|
|
83
|
+
// 3. Ask user to approve in browser
|
|
84
|
+
// --------------------------------------------------
|
|
47
85
|
note(
|
|
48
|
-
`${pc.bold("Pairing Code:")} ${pc.cyan(
|
|
86
|
+
`${pc.bold("Pairing Code:")} ${pc.cyan(
|
|
87
|
+
pc.bold(` ${userCode} `),
|
|
88
|
+
)}\n` +
|
|
49
89
|
`${pc.bold("Verification URL:")} ${pc.underline(verificationUrl)}`,
|
|
50
|
-
"Action Required"
|
|
90
|
+
"Action Required",
|
|
51
91
|
);
|
|
52
92
|
|
|
53
|
-
// 4. Auto-launch Browser
|
|
54
93
|
try {
|
|
55
94
|
await open(verificationUrl);
|
|
56
95
|
} catch {
|
|
57
|
-
//
|
|
96
|
+
// Browser launch failure is non-fatal.
|
|
58
97
|
}
|
|
59
98
|
|
|
60
|
-
//
|
|
99
|
+
// --------------------------------------------------
|
|
100
|
+
// 4. Poll for authorization
|
|
101
|
+
// --------------------------------------------------
|
|
61
102
|
s.start("Waiting for web authorization...");
|
|
62
103
|
|
|
63
104
|
let token: string | null = null;
|
|
105
|
+
|
|
64
106
|
const pollInterval = (interval || 5) * 1000;
|
|
65
107
|
|
|
66
108
|
while (!token) {
|
|
@@ -70,9 +112,14 @@ export const login = new Command()
|
|
|
70
112
|
`${API_BASE_URL}/api/auth/device/token`,
|
|
71
113
|
{
|
|
72
114
|
method: "POST",
|
|
73
|
-
headers: {
|
|
74
|
-
|
|
75
|
-
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
deviceCode,
|
|
120
|
+
machineId: hardwareId,
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
76
123
|
);
|
|
77
124
|
|
|
78
125
|
const tokenData = await tokenResponse.json();
|
|
@@ -81,23 +128,29 @@ export const login = new Command()
|
|
|
81
128
|
token = tokenData.accessToken;
|
|
82
129
|
} else if (tokenData.error !== "authorization_pending") {
|
|
83
130
|
s.stop(pc.red("Authorization failed or expired."));
|
|
131
|
+
|
|
84
132
|
cancel(`Reason: ${tokenData.error || "Unknown authorization error"}`);
|
|
133
|
+
|
|
85
134
|
process.exit(1);
|
|
86
135
|
}
|
|
87
136
|
}
|
|
88
137
|
|
|
89
|
-
//
|
|
138
|
+
// --------------------------------------------------
|
|
139
|
+
// 5. Save CLI token + machine ID
|
|
140
|
+
// --------------------------------------------------
|
|
90
141
|
saveToken(token, hardwareId);
|
|
91
142
|
|
|
92
143
|
s.stop(pc.green("Hardware signature linked & token verified!"));
|
|
93
144
|
|
|
94
|
-
// 7. Clean Success Outro
|
|
95
145
|
outro(
|
|
96
|
-
`${pc.white("✔")} ${pc.bold("Terminal synced successfully")}
|
|
146
|
+
`${pc.white("✔")} ${pc.bold("Terminal synced successfully")}\n` +
|
|
147
|
+
pc.gray("Secure device key and session token are ready."),
|
|
97
148
|
);
|
|
98
149
|
} catch (error: any) {
|
|
99
150
|
s.stop(pc.red("An error occurred during login."));
|
|
100
|
-
|
|
151
|
+
|
|
152
|
+
cancel(pc.gray(error?.message || "Unknown CLI Error"));
|
|
153
|
+
|
|
101
154
|
process.exit(1);
|
|
102
155
|
}
|
|
103
|
-
});
|
|
156
|
+
});
|
package/src/commands/logout.ts
CHANGED
|
@@ -2,28 +2,57 @@ 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
|
|
|
6
9
|
export const logout = new Command()
|
|
7
10
|
.name("logout")
|
|
8
|
-
.description("Log out of your DS01 account and
|
|
11
|
+
.description("Log out of your DS01 account and revoke the CLI session")
|
|
9
12
|
.action(async () => {
|
|
10
13
|
const token = getToken();
|
|
14
|
+
const machineId = machineIdSync();
|
|
11
15
|
|
|
12
|
-
if (!token) {
|
|
16
|
+
if (!token || !machineId) {
|
|
13
17
|
console.log(chalk.yellow("You are already logged out."));
|
|
14
18
|
return;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const spinner = ora("
|
|
21
|
+
const spinner = ora("Revoking CLI session...").start();
|
|
18
22
|
|
|
19
23
|
try {
|
|
20
|
-
|
|
24
|
+
const response = await fetch(`${API_BASE_URL}/api/auth/cli/logout`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token}`,
|
|
28
|
+
"X-Machine-ID": machineId,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// NEW: Do not silently delete local credentials if the server
|
|
33
|
+
// failed to revoke the session.
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const data = await response.json().catch(() => null);
|
|
36
|
+
|
|
37
|
+
throw new Error(
|
|
38
|
+
data?.error || `Logout failed (HTTP ${response.status})`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// NEW: Delete local credentials only after the server confirms
|
|
43
|
+
// that the CLI session has been revoked.
|
|
44
|
+
console.log("✅ Server confirmed CLI session was revoked.");
|
|
21
45
|
deleteToken();
|
|
22
|
-
|
|
46
|
+
console.log("✅ Local credentials deleted.");
|
|
23
47
|
spinner.succeed(chalk.green("Successfully logged out."));
|
|
24
|
-
console.log(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.gray(
|
|
50
|
+
"Your CLI session has been revoked and local credentials have been cleared.",
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
spinner.fail(chalk.red("Failed to log out."));
|
|
55
|
+
|
|
56
|
+
console.error(chalk.red(error.message || "Unknown logout error."));
|
|
28
57
|
}
|
|
29
|
-
});
|
|
58
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -22,4 +22,6 @@ program.addCommand(init);
|
|
|
22
22
|
program.addCommand(add); // <-- 2. Register the add command!
|
|
23
23
|
|
|
24
24
|
// Parse the arguments from the terminal
|
|
25
|
+
// It is the point where Commander reads the user's terminal input and decides
|
|
26
|
+
// which registered command's .action() function should execute.
|
|
25
27
|
program.parse(process.argv);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
let keyTag = "com.ds01.cli.device-key"
|
|
5
|
+
|
|
6
|
+
func fail(_ message: String) -> Never {
|
|
7
|
+
fputs(message + "\n", stderr)
|
|
8
|
+
exit(1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func getAccessControl() -> SecAccessControl {
|
|
12
|
+
var error: Unmanaged<CFError>?
|
|
13
|
+
|
|
14
|
+
guard let access = SecAccessControlCreateWithFlags(
|
|
15
|
+
nil,
|
|
16
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
17
|
+
.privateKeyUsage,
|
|
18
|
+
&error
|
|
19
|
+
) else {
|
|
20
|
+
if let error {
|
|
21
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fail("Unable to create Secure Enclave access control.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return access
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func findPrivateKey() -> SecKey? {
|
|
31
|
+
let query: [String: Any] = [
|
|
32
|
+
kSecClass as String: kSecClassKey,
|
|
33
|
+
kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
|
|
34
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
35
|
+
kSecReturnRef as String: true
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
var result: CFTypeRef?
|
|
39
|
+
|
|
40
|
+
let status = SecItemCopyMatching(
|
|
41
|
+
query as CFDictionary,
|
|
42
|
+
&result
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
guard status == errSecSuccess else {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (result as! SecKey)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func createKey() {
|
|
53
|
+
if findPrivateKey() != nil {
|
|
54
|
+
print("EXISTS")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let access = getAccessControl()
|
|
59
|
+
|
|
60
|
+
let attributes: [String: Any] = [
|
|
61
|
+
kSecAttrKeyType as String:
|
|
62
|
+
kSecAttrKeyTypeECSECPrimeRandom,
|
|
63
|
+
|
|
64
|
+
kSecAttrKeySizeInBits as String:
|
|
65
|
+
256,
|
|
66
|
+
|
|
67
|
+
kSecAttrTokenID as String:
|
|
68
|
+
kSecAttrTokenIDSecureEnclave,
|
|
69
|
+
|
|
70
|
+
kSecPrivateKeyAttrs as String: [
|
|
71
|
+
kSecAttrIsPermanent as String:
|
|
72
|
+
true,
|
|
73
|
+
|
|
74
|
+
kSecAttrApplicationTag as String:
|
|
75
|
+
keyTag.data(using: .utf8)!,
|
|
76
|
+
|
|
77
|
+
kSecAttrAccessControl as String:
|
|
78
|
+
access
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
var error: Unmanaged<CFError>?
|
|
83
|
+
|
|
84
|
+
guard SecKeyCreateRandomKey(
|
|
85
|
+
attributes as CFDictionary,
|
|
86
|
+
&error
|
|
87
|
+
) != nil else {
|
|
88
|
+
if let error {
|
|
89
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fail("Unable to create Secure Enclave key.")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
print("CREATED")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func publicKey() {
|
|
99
|
+
guard let privateKey = findPrivateKey() else {
|
|
100
|
+
fail("DS01 Secure Enclave key not found.")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
|
104
|
+
fail("Unable to obtain public key.")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var error: Unmanaged<CFError>?
|
|
108
|
+
|
|
109
|
+
guard let data = SecKeyCopyExternalRepresentation(
|
|
110
|
+
publicKey,
|
|
111
|
+
&error
|
|
112
|
+
) else {
|
|
113
|
+
if let error {
|
|
114
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fail("Unable to export public key.")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let base64 = (data as Data).base64EncodedString()
|
|
121
|
+
|
|
122
|
+
print(base64)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func sign(_ base64Data: String) {
|
|
126
|
+
guard let privateKey = findPrivateKey() else {
|
|
127
|
+
fail("DS01 Secure Enclave key not found.")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
guard let data = Data(base64Encoded: base64Data) else {
|
|
131
|
+
fail("Invalid input data.")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let algorithm = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
|
|
135
|
+
|
|
136
|
+
guard SecKeyIsAlgorithmSupported(
|
|
137
|
+
privateKey,
|
|
138
|
+
.sign,
|
|
139
|
+
algorithm
|
|
140
|
+
) else {
|
|
141
|
+
fail("Secure Enclave key does not support signing.")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var error: Unmanaged<CFError>?
|
|
145
|
+
|
|
146
|
+
guard let signature = SecKeyCreateSignature(
|
|
147
|
+
privateKey,
|
|
148
|
+
algorithm,
|
|
149
|
+
data as CFData,
|
|
150
|
+
&error
|
|
151
|
+
) else {
|
|
152
|
+
if let error {
|
|
153
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fail("Secure Enclave signing failed.")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
print((signature as Data).base64EncodedString())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func deleteKey() {
|
|
163
|
+
let query: [String: Any] = [
|
|
164
|
+
kSecClass as String: kSecClassKey,
|
|
165
|
+
kSecAttrApplicationTag as String:
|
|
166
|
+
keyTag.data(using: .utf8)!,
|
|
167
|
+
kSecAttrKeyType as String:
|
|
168
|
+
kSecAttrKeyTypeECSECPrimeRandom
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
let status = SecItemDelete(
|
|
172
|
+
query as CFDictionary
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if status != errSecSuccess &&
|
|
176
|
+
status != errSecItemNotFound {
|
|
177
|
+
fail("Unable to delete DS01 Secure Enclave key.")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
print("DELETED")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
guard CommandLine.arguments.count >= 2 else {
|
|
184
|
+
fail("Missing operation.")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
switch CommandLine.arguments[1] {
|
|
188
|
+
case "create":
|
|
189
|
+
createKey()
|
|
190
|
+
|
|
191
|
+
case "public":
|
|
192
|
+
publicKey()
|
|
193
|
+
|
|
194
|
+
case "sign":
|
|
195
|
+
guard CommandLine.arguments.count >= 3 else {
|
|
196
|
+
fail("Missing data to sign.")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sign(CommandLine.arguments[2])
|
|
200
|
+
|
|
201
|
+
case "delete":
|
|
202
|
+
deleteKey()
|
|
203
|
+
|
|
204
|
+
default:
|
|
205
|
+
fail("Unknown operation.")
|
|
206
|
+
}
|
package/src/test-key.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeviceKey,
|
|
3
|
+
getDevicePublicKey,
|
|
4
|
+
signWithDeviceKey,
|
|
5
|
+
} from "./utils/secureKeyStore.js";
|
|
6
|
+
|
|
7
|
+
await createDeviceKey();
|
|
8
|
+
|
|
9
|
+
const publicKey = await getDevicePublicKey();
|
|
10
|
+
|
|
11
|
+
console.log("Public key:", publicKey);
|
|
12
|
+
|
|
13
|
+
const message = new TextEncoder().encode("hello DS01");
|
|
14
|
+
|
|
15
|
+
const signature = await signWithDeviceKey(message);
|
|
16
|
+
|
|
17
|
+
console.log(
|
|
18
|
+
"Signature:",
|
|
19
|
+
Buffer.from(signature).toString("base64"),
|
|
20
|
+
);
|
package/src/utils/config.ts
CHANGED
|
@@ -35,18 +35,7 @@ export function getToken(): string | null {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
if (fs.existsSync(credentialsPath)) {
|
|
40
|
-
try {
|
|
41
|
-
const data = fs.readFileSync(credentialsPath, "utf-8");
|
|
42
|
-
const parsed = JSON.parse(data);
|
|
43
|
-
return parsed.machineId || null;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
38
|
+
|
|
50
39
|
|
|
51
40
|
export function deleteToken() {
|
|
52
41
|
if (fs.existsSync(credentialsPath)) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { webcrypto } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const { subtle } = webcrypto;
|
|
4
|
+
|
|
5
|
+
export async function generateDeviceKeyPair() {
|
|
6
|
+
// Generate an asymmetric key pair:
|
|
7
|
+
// - publicKey → can be shared with the server
|
|
8
|
+
// - privateKey → stays on this device
|
|
9
|
+
const keyPair = await subtle.generateKey(
|
|
10
|
+
{
|
|
11
|
+
name: "ECDSA",
|
|
12
|
+
namedCurve: "P-256",
|
|
13
|
+
},
|
|
14
|
+
false, // 🔐 Private key is NON-EXPORTABLE
|
|
15
|
+
["sign", "verify"],
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return keyPair;
|
|
19
|
+
}
|