@hellolynk-ai/codex-plugin 0.2.101 → 0.2.102-macos.1
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/README.md +17 -17
- package/bin/hellolynk-client-setup.mjs +72 -11
- package/hellolynk-client-pack-manifest.json +109 -69
- package/hellolynk-client-pack-pin.json +1 -1
- package/package.json +8 -4
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/hellolynk-codex/.mcp.json +20 -19
- package/plugins/hellolynk-codex/package.json +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +25 -2
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-vault.mjs +153 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +20 -8
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +3 -7
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +68 -14
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +5 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +25 -2
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +7 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +24 -8
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +23 -9
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-vault.mjs +153 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +20 -8
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +22 -7
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +3 -7
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +78 -2
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +5 -3
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +58 -26
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +149 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +15 -9
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -1193
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -33
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +42 -42
- package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { isDeepStrictEqual as equal } from "node:util";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { checkDarwinPath, vaultError, withDarwinVault } from "./hellolynk-darwin-vault.mjs";
|
|
4
|
+
|
|
5
|
+
const permanent = 253402300799999;
|
|
6
|
+
const month = 30 * 86400000;
|
|
7
|
+
function fail(code) { throw vaultError(code); }
|
|
8
|
+
function fieldsMatch(a, b, fields) { return fields.every(field => equal(a[field], b[field])); }
|
|
9
|
+
function sameOwner(a, b) { return fieldsMatch(a, b, ["principalId", "hostInstallationId"]); }
|
|
10
|
+
function used(records, field, id) { return records.some(r => r[field].toLowerCase() === id.toLowerCase()); }
|
|
11
|
+
function unique(records, missing, ambiguous) {
|
|
12
|
+
if (records.length !== 1) fail(records.length ? ambiguous : missing);
|
|
13
|
+
return records[0];
|
|
14
|
+
}
|
|
15
|
+
function checkBindingPaths(b) {
|
|
16
|
+
try {
|
|
17
|
+
checkDarwinPath(b.runtimeRoot, { directory: true });
|
|
18
|
+
checkDarwinPath(b.releaseRoot, { directory: true });
|
|
19
|
+
if (checkDarwinPath(b.profileManifestPath).size > 65536 ||
|
|
20
|
+
checkDarwinPath(join(b.releaseRoot, b.runnerRelativePath)).size > 1048576) fail("neutral_codex_launch_manifest_invalid");
|
|
21
|
+
} catch { fail("neutral_codex_launch_manifest_invalid"); }
|
|
22
|
+
}
|
|
23
|
+
function capMatches(cap, b) {
|
|
24
|
+
return cap.state === "active" && fieldsMatch(cap, b,
|
|
25
|
+
["region", "principalId", "delegateId", "hostInstallationId", "adapterId", "version", "scopes"]);
|
|
26
|
+
}
|
|
27
|
+
function addCapability(s, r) {
|
|
28
|
+
if (used(s.caps, "capabilityId", r.capabilityId)) fail("host_capability_id_conflict");
|
|
29
|
+
s.caps.push(r);
|
|
30
|
+
}
|
|
31
|
+
function revokeCapability(s, r) {
|
|
32
|
+
if (!r) fail("host_capability_missing");
|
|
33
|
+
if (r.state === "active") {
|
|
34
|
+
const revokedAt = Date.now();
|
|
35
|
+
if (revokedAt < r.issuedAt) fail("host_capability_state_invalid");
|
|
36
|
+
// Keep the identifier spent while discarding the retired secret.
|
|
37
|
+
r = s.caps[s.caps.indexOf(r)] = { capabilityId: r.capabilityId, issuedAt: r.issuedAt, state: "revoked", revokedAt };
|
|
38
|
+
}
|
|
39
|
+
return { capabilityId: r.capabilityId, revokedAt: r.revokedAt, state: "revoked" };
|
|
40
|
+
}
|
|
41
|
+
function codexBinding(s, command, input, oldId) {
|
|
42
|
+
const b = s.codex;
|
|
43
|
+
if (command === "ResolveCodexNeutralBinding" || command === "VerifyCodexNeutralBinding") {
|
|
44
|
+
if (!b) fail("neutral_codex_binding_configuration_required");
|
|
45
|
+
checkBindingPaths(b);
|
|
46
|
+
const active = s.caps.filter(c => c.adapterId === "codex" && c.state === "active");
|
|
47
|
+
const cap = unique(active.filter(c => capMatches(c, b)),
|
|
48
|
+
active.length ? "neutral_codex_capability_binding_mismatch" : "neutral_codex_capability_missing", "neutral_codex_capability_ambiguous");
|
|
49
|
+
if (command.startsWith("Resolve")) return { binding: b, record: cap };
|
|
50
|
+
const {secret, ...record} = cap;
|
|
51
|
+
return { binding: b, record };
|
|
52
|
+
}
|
|
53
|
+
if (command === "SaveCodexNeutralBinding" || command === "RemoveCodexNeutralBinding") {
|
|
54
|
+
checkBindingPaths(input);
|
|
55
|
+
if (command.startsWith("Remove")) {
|
|
56
|
+
if (!equal(b, input)) fail("neutral_codex_binding_invalid");
|
|
57
|
+
s.codex = null;
|
|
58
|
+
return { removed: true };
|
|
59
|
+
}
|
|
60
|
+
if (b && !equal(b, input)) fail("neutral_codex_binding_invalid");
|
|
61
|
+
s.codex = input;
|
|
62
|
+
return { binding: input };
|
|
63
|
+
}
|
|
64
|
+
const { previousBinding: previous, replacementBinding: next, replacementRecord } = input;
|
|
65
|
+
checkBindingPaths(previous);
|
|
66
|
+
checkBindingPaths(next);
|
|
67
|
+
if (!fieldsMatch(previous, next, ["contract","region","principalId","delegateId","hostInstallationId","profileId","adapterId","version","runtimeRoot"])) {
|
|
68
|
+
fail("neutral_codex_capability_binding_mismatch");
|
|
69
|
+
}
|
|
70
|
+
if (!equal(b, previous)) fail("neutral_codex_binding_invalid");
|
|
71
|
+
const active = s.caps.filter(c => capMatches(c, next));
|
|
72
|
+
if (command === "ReplaceCodexNeutralBinding") {
|
|
73
|
+
if (!equal(previous.scopes, next.scopes)) fail("neutral_codex_capability_binding_mismatch");
|
|
74
|
+
if (active.length !== 1) fail("neutral_codex_capability_ambiguous");
|
|
75
|
+
s.codex = next;
|
|
76
|
+
return { binding: next };
|
|
77
|
+
}
|
|
78
|
+
if (command !== "MigrateCodexNeutralBinding") fail("host_protected_store_command_invalid");
|
|
79
|
+
const old = s.caps.find(c => c.capabilityId === oldId);
|
|
80
|
+
if (!old || !capMatches(old, previous) || !capMatches(replacementRecord, next)) fail("neutral_codex_capability_binding_mismatch");
|
|
81
|
+
const other = active.filter(c => c.capabilityId !== oldId);
|
|
82
|
+
if (other.length > 1) fail("neutral_codex_capability_ambiguous");
|
|
83
|
+
if (!other.length) addCapability(s, replacementRecord);
|
|
84
|
+
revokeCapability(s, old);
|
|
85
|
+
s.codex = next;
|
|
86
|
+
return { binding: next, record: other[0] || replacementRecord, replacedCapabilityId: oldId };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function retireSession(s, r) {
|
|
90
|
+
s.sessions.splice(s.sessions.indexOf(r), 1);
|
|
91
|
+
s.retiredSessions.push(r.sessionId.toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
function revokeCredential(s, r, timestamp) {
|
|
94
|
+
if (timestamp < r.issuedAt) fail("host_transport_credential_revoke_timestamp_invalid");
|
|
95
|
+
const sessions = s.sessions.filter(session => session.credentialId === r.credentialId);
|
|
96
|
+
if (r.state === "active") {
|
|
97
|
+
sessions.forEach(session => retireSession(s, session));
|
|
98
|
+
const { credentialId, issuedAt, principalId, hostInstallationId } = r;
|
|
99
|
+
r = s.credentials[s.credentials.indexOf(r)] = { credentialId, issuedAt, principalId, hostInstallationId, state:"revoked", revokedAt:timestamp };
|
|
100
|
+
}
|
|
101
|
+
return { credentialId:r.credentialId, state:"revoked", revokedAt:r.revokedAt, cascadedSessionCount:sessions.length };
|
|
102
|
+
}
|
|
103
|
+
function exactFields(value, fields) {
|
|
104
|
+
return value && typeof value === "object" && equal(Object.keys(value).sort(), fields.sort());
|
|
105
|
+
}
|
|
106
|
+
function checkEnrollment(value, record) {
|
|
107
|
+
const a = value?.approval, approved = a?.hostTransportCredential;
|
|
108
|
+
if (!exactFields(value, ["baseUrl","hellolynkId","hostId","deviceLabel","requestId","proof","approval","intents"])
|
|
109
|
+
|| !exactFields(a, ["status","principalId","hellolynkId","region","hostId","hostTransportCredential"])
|
|
110
|
+
|| !exactFields(approved, ["credentialId","credential","createdAt","expiresAt"])
|
|
111
|
+
|| a.status !== "approved" || a.principalId !== record.principalId || a.hostId !== record.hostInstallationId
|
|
112
|
+
|| value.hostId !== a.hostId || a.hellolynkId !== value.hellolynkId || approved.credentialId !== record.credentialId
|
|
113
|
+
|| approved.credential !== record.secret || Date.parse(approved.createdAt) !== record.issuedAt
|
|
114
|
+
|| Date.parse(approved.expiresAt) !== record.expiresAt) fail("device_enrollment_transport_approval_mismatch");
|
|
115
|
+
if (record.expiresAt <= Date.now()) fail("host_transport_credential_expired");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function transport(s, q, input) {
|
|
119
|
+
const {command, timestamp} = q;
|
|
120
|
+
const id = q.credentialId || input?.credentialId;
|
|
121
|
+
const credential = s.credentials.find(r => r.credentialId === id);
|
|
122
|
+
const owned = query => s.credentials.filter(r => r.state === "active" && sameOwner(r, query));
|
|
123
|
+
if (command === "SaveTransportCredential" || command === "SaveEnrollmentTransportCredential") {
|
|
124
|
+
const upgrade = command === "SaveEnrollmentTransportCredential";
|
|
125
|
+
if (upgrade) checkEnrollment(s.enrollment.value, input);
|
|
126
|
+
if (credential) {
|
|
127
|
+
if (!equal(credential,input)) fail("host_transport_credential_immutable_conflict");
|
|
128
|
+
} else {
|
|
129
|
+
if (used(s.credentials, "credentialId", id)) fail("host_transport_credential_immutable_conflict");
|
|
130
|
+
const old = owned(input);
|
|
131
|
+
if (old.length > 1 || (old.length && !upgrade)) fail("host_transport_credential_ambiguous");
|
|
132
|
+
if (old.length) revokeCredential(s,old[0],Date.now());
|
|
133
|
+
s.credentials.push(input);
|
|
134
|
+
}
|
|
135
|
+
return { record: input };
|
|
136
|
+
}
|
|
137
|
+
if (command === "VerifyTransportCredentialLifetimeMigration" || command === "MigrateTransportCredentialLifetime") {
|
|
138
|
+
const record = unique(owned(q), "host_transport_migration_credential_inactive_or_ambiguous", "host_transport_migration_credential_inactive_or_ambiguous");
|
|
139
|
+
if (record.expiresAt !== permanent && record.expiresAt - record.issuedAt !== month) fail("host_transport_migration_lifetime_invalid");
|
|
140
|
+
if (command.startsWith("Verify")) return { record };
|
|
141
|
+
if (record.expiresAt-record.issuedAt !== month || !equal(record,input.previous)) fail("host_transport_migration_compare_failed");
|
|
142
|
+
const proof = input.proof;
|
|
143
|
+
if (proof.credentialId !== record.credentialId || proof.hostId !== q.hostInstallationId || proof.principalId !== q.principalId
|
|
144
|
+
|| Date.parse(proof.credentialCreatedAt) !== record.issuedAt || Date.parse(proof.credentialExpiresAt) !== permanent) fail("host_transport_migration_proof_invalid");
|
|
145
|
+
record.expiresAt = permanent;
|
|
146
|
+
return { credentialId: record.credentialId, expiresAt: permanent, status:"migrated" };
|
|
147
|
+
}
|
|
148
|
+
if (command === "LoadTransportCredential" || command === "RevokeTransportCredential") {
|
|
149
|
+
if (command.startsWith("Load") && credential?.state !== "active") fail("host_transport_credential_inactive");
|
|
150
|
+
if (!credential) fail("host_transport_credential_missing");
|
|
151
|
+
if (!sameOwner(credential,q)) fail("host_transport_credential_owner_mismatch");
|
|
152
|
+
if (command.startsWith("Revoke")) return revokeCredential(s,credential,timestamp);
|
|
153
|
+
if (credential.expiresAt <= timestamp) fail("host_transport_credential_expired");
|
|
154
|
+
return { record: credential };
|
|
155
|
+
}
|
|
156
|
+
const sessionId = q.sessionId || input?.sessionId;
|
|
157
|
+
const session = s.sessions.find(r => r.sessionId === sessionId);
|
|
158
|
+
if (command === "SaveTransportSession" || command === "ReplaceTransportSession") {
|
|
159
|
+
if (credential?.state !== "active") fail("host_transport_credential_inactive");
|
|
160
|
+
if (!sameOwner(credential,input) || credential.expiresAt <= timestamp || input.expiresAt > credential.expiresAt) fail("host_transport_session_credential_mismatch");
|
|
161
|
+
if (s.retiredSessions.includes(sessionId.toLowerCase())) fail("host_transport_session_retired");
|
|
162
|
+
if (session) {
|
|
163
|
+
if (!equal(session,input)) fail("host_transport_session_immutable_conflict");
|
|
164
|
+
} else {
|
|
165
|
+
if (used(s.sessions, "sessionId", sessionId)) fail("host_transport_session_duplicate");
|
|
166
|
+
const previous = s.sessions.filter(r => sameOwner(r,input));
|
|
167
|
+
if (previous.length && command.startsWith("Save")) fail("host_transport_session_ambiguous");
|
|
168
|
+
previous.forEach(r => retireSession(s,r));
|
|
169
|
+
s.sessions.push(input);
|
|
170
|
+
}
|
|
171
|
+
return { record: input };
|
|
172
|
+
}
|
|
173
|
+
if (command === "LoadTransportSession" || command === "RemoveTransportSession") {
|
|
174
|
+
if (session && !sameOwner(session,q)) fail("host_transport_session_owner_mismatch");
|
|
175
|
+
if (command.startsWith("Remove")) {
|
|
176
|
+
if (session) retireSession(s,session);
|
|
177
|
+
return { sessionId, removed: Boolean(session) };
|
|
178
|
+
}
|
|
179
|
+
if (!session) fail("host_transport_session_invalid");
|
|
180
|
+
const parent = s.credentials.find(c => c.credentialId === session.credentialId);
|
|
181
|
+
if (parent?.state !== "active" || parent.expiresAt <= timestamp) fail("host_transport_credential_inactive");
|
|
182
|
+
if (session.expiresAt <= timestamp) fail("host_transport_session_expired");
|
|
183
|
+
return { record: session };
|
|
184
|
+
}
|
|
185
|
+
if (command === "ResolveTransport" || command === "VerifyTransport") {
|
|
186
|
+
const c = unique(owned(q),"host_transport_credential_required","host_transport_credential_ambiguous");
|
|
187
|
+
if (c.expiresAt <= timestamp) fail("host_transport_credential_expired");
|
|
188
|
+
const sessions = s.sessions.filter(r => r.credentialId === c.credentialId && sameOwner(r,q));
|
|
189
|
+
if (sessions.length > 1) fail("host_transport_session_ambiguous");
|
|
190
|
+
let selected = sessions[0] || null;
|
|
191
|
+
if (selected && selected.expiresAt <= timestamp) {
|
|
192
|
+
if (command.startsWith("Resolve")) retireSession(s,selected);
|
|
193
|
+
selected = null;
|
|
194
|
+
}
|
|
195
|
+
if (command.startsWith("Resolve")) return { credential:c, session:selected };
|
|
196
|
+
const publicFields = ({credentialId,principalId,hostInstallationId,expiresAt}) => ({credentialId,principalId,hostInstallationId,expiresAt});
|
|
197
|
+
return { credential:publicFields(c), session:selected && {...publicFields(selected),sessionId:selected.sessionId} };
|
|
198
|
+
}
|
|
199
|
+
fail("host_protected_store_command_invalid");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emptyHost(enrollment = { contract:"hellolynk-device-enrollment-v1", revision:0, value:null }) {
|
|
203
|
+
return { caps:[], credentials:[], sessions:[], retiredSessions:[], codex:null, enrollment };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// On Windows the enrollment file exists independently of the capability file.
|
|
207
|
+
// An unclaimed Mac Host may have only its enrollment in the shared vault. Read
|
|
208
|
+
// its authenticated state without creating, repairing or replacing anything.
|
|
209
|
+
export function verifyDarwinUnclaimedAuthorization(root) {
|
|
210
|
+
return withDarwinVault({ root, readOnly:true }, document => {
|
|
211
|
+
const enrollment = document?.host?.enrollment;
|
|
212
|
+
if (!exactFields(enrollment, ["contract", "revision", "value"])
|
|
213
|
+
|| enrollment.contract !== "hellolynk-device-enrollment-v1"
|
|
214
|
+
|| !Number.isSafeInteger(enrollment.revision) || enrollment.revision < 0
|
|
215
|
+
|| (enrollment.value !== null && (typeof enrollment.value !== "object" || Array.isArray(enrollment.value)))) {
|
|
216
|
+
fail("device_enrollment_store_invalid");
|
|
217
|
+
}
|
|
218
|
+
if (!equal(document, { host:emptyHost(enrollment) })) fail("host_unclaimed_authorization_present");
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function invokeDarwinHost(q) {
|
|
223
|
+
const readOnly = q.verifyOnly || q.command.startsWith("Verify");
|
|
224
|
+
return withDarwinVault({ root:q.root, helperPath:q.helperPath, readOnly, signal:q.signal }, document => {
|
|
225
|
+
const s = document.host || emptyHost();
|
|
226
|
+
if (!readOnly) document.host = s;
|
|
227
|
+
const input = q.stdin ? JSON.parse(q.stdin) : null;
|
|
228
|
+
const old = s.caps.find(c => c.capabilityId === q.capabilityId);
|
|
229
|
+
switch (q.command) {
|
|
230
|
+
case "Status": return {contract:"hellolynk-host-protected-store-v1",protected:true};
|
|
231
|
+
case "LoadDeviceEnrollment": return s.enrollment;
|
|
232
|
+
case "SaveDeviceEnrollment":
|
|
233
|
+
if (s.enrollment.revision !== input.expectedRevision) fail("device_enrollment_revision_conflict");
|
|
234
|
+
s.enrollment.value = input.value;
|
|
235
|
+
s.enrollment.revision++;
|
|
236
|
+
return s.enrollment;
|
|
237
|
+
case "Issue": addCapability(s,input); return {record:input};
|
|
238
|
+
case "Load":
|
|
239
|
+
if (old?.state !== "active") fail("host_capability_inactive");
|
|
240
|
+
return {record:old};
|
|
241
|
+
case "Revoke": return revokeCapability(s,old);
|
|
242
|
+
case "Rotate":
|
|
243
|
+
if (old?.state !== "active") fail("host_capability_inactive");
|
|
244
|
+
addCapability(s,input);
|
|
245
|
+
revokeCapability(s,old);
|
|
246
|
+
return {record:input, replacedCapabilityId:q.capabilityId};
|
|
247
|
+
case "DisconnectLocalClient":
|
|
248
|
+
for (const cap of s.caps.filter(c => c.adapterId === input.client && c.state === "active")) revokeCapability(s,cap);
|
|
249
|
+
if (input.client === "codex") s.codex = null;
|
|
250
|
+
return {disconnected:true,client:input.client};
|
|
251
|
+
}
|
|
252
|
+
if (q.command.endsWith("CodexNeutralBinding")) return codexBinding(s,q.command,input,q.capabilityId);
|
|
253
|
+
if (q.command.includes("Transport")) return transport(s,q,input);
|
|
254
|
+
fail("host_protected_store_command_unsupported");
|
|
255
|
+
});
|
|
256
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// macOS-only storage. Keychain holds the wrapping key. The SQLite rollback
|
|
2
|
+
// journal and database contain a single authenticated, root-bound ciphertext.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
5
|
+
import { closeSync, constants, lstatSync, mkdirSync, openSync } from "node:fs";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
export const DARWIN_HELPER = fileURLToPath(new URL("../platform/darwin/hellolynk-keychain-helper.bin", import.meta.url));
|
|
11
|
+
export const DARWIN_VAULT_FILE = "protected-v1.sqlite";
|
|
12
|
+
// Neither the wrapping-key process nor the approval UI needs a caller's
|
|
13
|
+
// shell, language runtime, dynamic-loader overrides or credential environment.
|
|
14
|
+
export function darwinHelperEnvironment() {
|
|
15
|
+
return { HOME: homedir(), PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8" };
|
|
16
|
+
}
|
|
17
|
+
export function vaultError(code) { return Object.assign(new Error(code), { code }); }
|
|
18
|
+
|
|
19
|
+
// Validate before creating any missing components (including in a supplied
|
|
20
|
+
// storeRoot). Never follow links or create children below a linked ancestor.
|
|
21
|
+
export function checkDarwinPath(target, { directory = false, privateMode = false, create = false, missing = false } = {}) {
|
|
22
|
+
if (target === "/" || target !== resolve(target)) throw vaultError("macos_vault_path_invalid");
|
|
23
|
+
const parts = target.slice(1).split("/");
|
|
24
|
+
let current = "/";
|
|
25
|
+
for (const [index, name] of parts.entries()) {
|
|
26
|
+
current = join(current, name);
|
|
27
|
+
const dir = index < parts.length - 1 || directory;
|
|
28
|
+
let st = lstatSync(current, { throwIfNoEntry: false });
|
|
29
|
+
if (!st && create && dir) {
|
|
30
|
+
try { mkdirSync(current, { mode: 0o700 }); }
|
|
31
|
+
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
32
|
+
st = lstatSync(current);
|
|
33
|
+
}
|
|
34
|
+
if (!st && missing) return null;
|
|
35
|
+
if (!st || st.isSymbolicLink() || (dir ? !st.isDirectory() : !st.isFile())) throw vaultError("macos_vault_path_invalid");
|
|
36
|
+
if (index < parts.length - 1 && ((st.uid !== 0 && st.uid !== process.getuid()) ||
|
|
37
|
+
((st.mode & 0o022) && !(st.uid === 0 && (st.mode & 0o1000))))) {
|
|
38
|
+
throw vaultError("macos_vault_ancestor_invalid");
|
|
39
|
+
}
|
|
40
|
+
if (index === parts.length - 1) {
|
|
41
|
+
if (privateMode && (st.uid !== process.getuid() || (st.mode & 0o077) || (!dir && st.nlink !== 1))) {
|
|
42
|
+
throw vaultError("macos_vault_permissions_invalid");
|
|
43
|
+
}
|
|
44
|
+
return st;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function invokeDarwinHelper(helper, args, signal) {
|
|
50
|
+
const st = checkDarwinPath(helper);
|
|
51
|
+
if ((st.mode & 0o022) || (st.uid !== 0 && st.uid !== process.getuid())) throw vaultError("macos_vault_helper_invalid");
|
|
52
|
+
signal?.throwIfAborted();
|
|
53
|
+
return new Promise((ok, reject) => {
|
|
54
|
+
const child = spawn(helper, args, { stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
55
|
+
env: darwinHelperEnvironment(), signal });
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let size = 0, stderr = "";
|
|
58
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 30_000);
|
|
59
|
+
child.stdio[3].on("data", chunk => {
|
|
60
|
+
size += chunk.length;
|
|
61
|
+
if (size <= 32) chunks.push(chunk);
|
|
62
|
+
else child.kill("SIGKILL");
|
|
63
|
+
});
|
|
64
|
+
child.stderr.on("data", chunk => {
|
|
65
|
+
stderr += chunk;
|
|
66
|
+
if (stderr.length > 256) child.kill("SIGKILL");
|
|
67
|
+
});
|
|
68
|
+
child.on("error", error => { clearTimeout(timer); reject(error); });
|
|
69
|
+
child.on("close", code => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
const data = Buffer.concat(chunks);
|
|
72
|
+
chunks.forEach(chunk => chunk.fill(0));
|
|
73
|
+
if (code === 0 && data.length === (args[0] === "key" ? 32 : 0)) return ok(data);
|
|
74
|
+
data.fill(0);
|
|
75
|
+
reject(vaultError(/^macos_[a-z0-9_]{1,100}$/.test(stderr.trim()) ? stderr.trim() : "macos_vault_helper_failed"));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openDocument(key, aad, bytes) {
|
|
81
|
+
try {
|
|
82
|
+
const blob = Buffer.from(bytes);
|
|
83
|
+
const cipher = createDecipheriv("aes-256-gcm", key, blob.subarray(0, 12));
|
|
84
|
+
cipher.setAAD(aad);
|
|
85
|
+
cipher.setAuthTag(blob.subarray(12, 28));
|
|
86
|
+
return Buffer.concat([cipher.update(blob.subarray(28)), cipher.final()]);
|
|
87
|
+
} catch { throw vaultError("macos_vault_authentication_failed"); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sealDocument(key, aad, plain) {
|
|
91
|
+
const nonce = randomBytes(12);
|
|
92
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
93
|
+
cipher.setAAD(aad);
|
|
94
|
+
const body = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
95
|
+
return Buffer.concat([nonce, cipher.getAuthTag(), body]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The callback is synchronous. No event-loop work or network I/O is performed
|
|
99
|
+
// while SQLite owns the write lock. Concurrent first writers share SecItemAdd's
|
|
100
|
+
// winning key and re-read the committed document after BEGIN IMMEDIATE.
|
|
101
|
+
export async function withDarwinVault({ root, helperPath = DARWIN_HELPER, readOnly = false, allowMissing = false, signal }, operation) {
|
|
102
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
103
|
+
const directory = checkDarwinPath(root, { directory: true, privateMode: true, create: !readOnly, missing: readOnly && allowMissing });
|
|
104
|
+
const path = join(root, DARWIN_VAULT_FILE);
|
|
105
|
+
let stat = directory && checkDarwinPath(path, { privateMode: true, missing: true });
|
|
106
|
+
if (!stat) {
|
|
107
|
+
if (readOnly) {
|
|
108
|
+
if (allowMissing) return operation({});
|
|
109
|
+
throw vaultError("host_protected_store_path_invalid");
|
|
110
|
+
}
|
|
111
|
+
try { closeSync(openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600)); }
|
|
112
|
+
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
113
|
+
stat = checkDarwinPath(path, { privateMode: true });
|
|
114
|
+
}
|
|
115
|
+
const newFile = stat.size === 0;
|
|
116
|
+
const key = await invokeDarwinHelper(helperPath, ["key", root, !readOnly && newFile ? "create" : "load"], signal);
|
|
117
|
+
let db, plain, encoded;
|
|
118
|
+
try {
|
|
119
|
+
signal?.throwIfAborted();
|
|
120
|
+
checkDarwinPath(path, { privateMode: true });
|
|
121
|
+
checkDarwinPath(path + "-journal", { privateMode: true, missing: true });
|
|
122
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
123
|
+
if (lstatSync(path + suffix, { throwIfNoEntry: false })) throw vaultError("macos_vault_journal_invalid");
|
|
124
|
+
}
|
|
125
|
+
db = new DatabaseSync(path, { readOnly, allowExtension: false });
|
|
126
|
+
db.exec("PRAGMA trusted_schema=OFF; PRAGMA busy_timeout=10000");
|
|
127
|
+
if (db.prepare("PRAGMA journal_mode").get().journal_mode !== "delete") throw vaultError("macos_vault_journal_invalid");
|
|
128
|
+
db.exec(readOnly ? "BEGIN" : "BEGIN IMMEDIATE");
|
|
129
|
+
const schema = db.prepare("SELECT sql FROM sqlite_schema WHERE name = 'vault' AND type = 'table'").get();
|
|
130
|
+
if (!schema) {
|
|
131
|
+
if (!newFile || readOnly) throw vaultError("macos_vault_format_invalid");
|
|
132
|
+
db.exec("CREATE TABLE vault (id INTEGER PRIMARY KEY CHECK(id=1), sealed BLOB NOT NULL)");
|
|
133
|
+
}
|
|
134
|
+
const row = db.prepare("SELECT sealed FROM vault WHERE id=1").get();
|
|
135
|
+
if (schema && !row) throw vaultError("macos_vault_format_invalid");
|
|
136
|
+
const aad = Buffer.from("hellolynk-macos-vault-v1\0" + root);
|
|
137
|
+
plain = row ? openDocument(key, aad, row.sealed) : Buffer.from("{}");
|
|
138
|
+
const state = JSON.parse(plain);
|
|
139
|
+
const result = operation(state);
|
|
140
|
+
encoded = Buffer.from(JSON.stringify(state));
|
|
141
|
+
if (!row || !plain.equals(encoded)) {
|
|
142
|
+
if (readOnly) throw vaultError("host_protected_store_read_only");
|
|
143
|
+
db.prepare("INSERT OR REPLACE INTO vault(id,sealed) VALUES(1,?)").run(sealDocument(key, aad, encoded));
|
|
144
|
+
}
|
|
145
|
+
db.exec("COMMIT");
|
|
146
|
+
return result;
|
|
147
|
+
} finally {
|
|
148
|
+
key.fill(0);
|
|
149
|
+
plain?.fill(0);
|
|
150
|
+
encoded?.fill(0);
|
|
151
|
+
db?.close(); // closes or rolls back a transaction whose callback failed
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { invokeDarwinHost } from "./hellolynk-darwin-host.mjs";
|
|
2
|
+
import { checkDarwinPath, DARWIN_HELPER, invokeDarwinHelper } from "./hellolynk-darwin-vault.mjs";
|
|
1
3
|
import { randomBytes } from "node:crypto";
|
|
2
4
|
import { lstat, mkdir } from "node:fs/promises";
|
|
3
5
|
import { createConnection } from "node:net";
|
|
@@ -142,7 +144,7 @@ const READ_ONLY_COMMANDS = new Set([
|
|
|
142
144
|
]);
|
|
143
145
|
|
|
144
146
|
export function defaultClaudeCodeStoreRoot(env = process.env) {
|
|
145
|
-
return defaultNeutralHostStoreRoot(
|
|
147
|
+
return defaultNeutralHostStoreRoot(process.platform, env);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
function assertClaudeCodeStoreRoot(root) {
|
|
@@ -154,9 +156,9 @@ function assertClaudeCodeStoreRoot(root) {
|
|
|
154
156
|
|
|
155
157
|
export function createHostProtectedStore(options = {}) {
|
|
156
158
|
const platform = options.platform || process.platform;
|
|
157
|
-
if (
|
|
159
|
+
if (!["darwin", "win32"].includes(platform)) throw codeError("host_protected_store_platform_unsupported");
|
|
158
160
|
const root = assertRoot(options.root || defaultNeutralHostStoreRoot(platform, options.env));
|
|
159
|
-
const helperPath = resolve(options.helperPath || SCRIPT_PATH);
|
|
161
|
+
const helperPath = resolve(options.helperPath || (platform === "darwin" ? DARWIN_HELPER : SCRIPT_PATH));
|
|
160
162
|
const clock = normalizeClock(options.clock);
|
|
161
163
|
const random = typeof options.randomBytes === "function" ? options.randomBytes : randomBytes;
|
|
162
164
|
const readOnly = options.readOnly === true;
|
|
@@ -166,12 +168,12 @@ export function createHostProtectedStore(options = {}) {
|
|
|
166
168
|
verifyPath: ({ path, directory }) => verifyHostProtectedStorePath({
|
|
167
169
|
root,
|
|
168
170
|
path,
|
|
169
|
-
directory,
|
|
171
|
+
directory, platform,
|
|
170
172
|
helperPath,
|
|
171
173
|
signal: options.signal,
|
|
172
174
|
}),
|
|
173
175
|
} : { root });
|
|
174
|
-
const nativeInvoke = (command, invokeOptions) => invokeWindowsStore({
|
|
176
|
+
const nativeInvoke = (command, invokeOptions) => (platform === "darwin" ? invokeDarwinHost : invokeWindowsStore)({
|
|
175
177
|
command, root, helperPath, verifyOnly: readOnly, ...(readOnly ? { signal: options.signal } : {}), ...invokeOptions,
|
|
176
178
|
});
|
|
177
179
|
const baseInvoke = options.invoke || ((command, invokeOptions) => serializeRootInvoke(root, () => nativeInvoke(command, invokeOptions)));
|
|
@@ -180,7 +182,7 @@ export function createHostProtectedStore(options = {}) {
|
|
|
180
182
|
// Each peer lookup creates a read-only store. Reuse its successful DPAPI
|
|
181
183
|
// result across those instances, while checking live paths and the file
|
|
182
184
|
// on every use. Registration/account checks remain in the public methods.
|
|
183
|
-
if (readOnly && !options.invoke && NEUTRAL_BINDING_READ.test(command)) {
|
|
185
|
+
if (platform === "win32" && readOnly && !options.invoke && NEUTRAL_BINDING_READ.test(command)) {
|
|
184
186
|
return readNeutralBinding(command, invokeOptions);
|
|
185
187
|
}
|
|
186
188
|
try {
|
|
@@ -271,6 +273,7 @@ export function createHostProtectedStore(options = {}) {
|
|
|
271
273
|
|
|
272
274
|
async function resolveCapability(capabilityId) {
|
|
273
275
|
const id = assertCapabilityId(capabilityId);
|
|
276
|
+
if (platform === "darwin") return currentCapability(await load(id));
|
|
274
277
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
275
278
|
const fingerprint = await readCapabilityStoreFingerprint();
|
|
276
279
|
if (fingerprint !== capabilityCacheFingerprint) {
|
|
@@ -1347,13 +1350,22 @@ async function protectedPathFingerprint(root, target, helperPath, directory, sig
|
|
|
1347
1350
|
}
|
|
1348
1351
|
|
|
1349
1352
|
export async function verifyHostProtectedStorePath(options = {}) {
|
|
1350
|
-
const
|
|
1353
|
+
const platform = options.platform || process.platform;
|
|
1354
|
+
const root = assertRoot(options.root || defaultNeutralHostStoreRoot(platform, options.env));
|
|
1351
1355
|
const target = resolve(String(options.path || ""));
|
|
1352
1356
|
const relativeTarget = relative(root, target);
|
|
1353
1357
|
if (!isAbsolute(String(options.path || ""))
|
|
1354
|
-
|| (relativeTarget !== "" && (isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith(`..\\`)))) {
|
|
1358
|
+
|| (relativeTarget !== "" && (isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith("../") || relativeTarget.startsWith(`..\\`)))) {
|
|
1355
1359
|
throw codeError("host_protected_store_path_invalid");
|
|
1356
1360
|
}
|
|
1361
|
+
if (platform === "darwin") {
|
|
1362
|
+
checkDarwinPath(root, { directory: true, privateMode: true });
|
|
1363
|
+
checkDarwinPath(target, { directory: options.directory === true, privateMode: true });
|
|
1364
|
+
const helper = options.helperPath || DARWIN_HELPER;
|
|
1365
|
+
await invokeDarwinHelper(helper, ["verify-acl", root], options.signal);
|
|
1366
|
+
if (target !== root) await invokeDarwinHelper(helper, ["verify-acl", target], options.signal);
|
|
1367
|
+
return Object.freeze({ verified: true });
|
|
1368
|
+
}
|
|
1357
1369
|
const helperPath = resolve(options.helperPath || SCRIPT_PATH);
|
|
1358
1370
|
const directory = options.directory === true;
|
|
1359
1371
|
const key = JSON.stringify([root, target, helperPath, directory]);
|
package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs
CHANGED
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
15
|
import { performance } from "node:perf_hooks";
|
|
17
16
|
import { withLocalProcessLock } from "./hellolynk-local-process-lock.mjs";
|
|
18
|
-
import { readClientProfile } from "./hellolynk-client-profile.mjs";
|
|
17
|
+
import { defaultHelloLynkClientRoot, readClientProfile } from "./hellolynk-client-profile.mjs";
|
|
19
18
|
import { assertIdentifier } from "./hellolynk-host-auth.mjs";
|
|
20
19
|
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
21
20
|
|
|
@@ -46,10 +45,7 @@ export function defaultNeutralHostStoreRoot(platform = process.platform, env = p
|
|
|
46
45
|
}
|
|
47
46
|
return root;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk", "host");
|
|
51
|
-
if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk", "host");
|
|
52
|
-
return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk", "host");
|
|
48
|
+
return join(defaultHelloLynkClientRoot(platform, env), "host");
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
export function createNeutralHostInstallationStore(options = {}) {
|
|
@@ -344,7 +340,7 @@ async function assertReadOnlyPath(path, directory, verifyPath) {
|
|
|
344
340
|
if (metadata.isSymbolicLink() || (directory ? !metadata.isDirectory() : !metadata.isFile())) {
|
|
345
341
|
throw codeError(directory ? "host_store_root_invalid" : "host_registry_path_invalid");
|
|
346
342
|
}
|
|
347
|
-
if (process.platform === "win32") {
|
|
343
|
+
if (process.platform === "win32" || (process.platform === "darwin" && verifyPath)) {
|
|
348
344
|
await verifyPath({ path, directory });
|
|
349
345
|
}
|
|
350
346
|
}
|
package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkDarwinPath, DARWIN_HELPER, invokeDarwinHelper, withDarwinVault } from "./hellolynk-darwin-vault.mjs";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
3
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -260,6 +261,14 @@ export async function runWindowsClientTrustCommand(command, options = {}) {
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
export async function prepareWindowsClientRoot(clientRoot, options = {}) {
|
|
264
|
+
if ((options.platform || process.platform) === "darwin") {
|
|
265
|
+
checkDarwinPath(clientRoot, { directory: true, create: true, privateMode: true });
|
|
266
|
+
for (const sub of ["profiles", "credentials", "e2ee-private-keys", "status", "host", "host/runtime", "releases"]) {
|
|
267
|
+
checkDarwinPath(join(clientRoot, sub), { directory: true, create: true, privateMode: true });
|
|
268
|
+
}
|
|
269
|
+
await invokeDarwinHelper(options.helperPath || DARWIN_HELPER, ["verify-acl", clientRoot]);
|
|
270
|
+
return "prepared";
|
|
271
|
+
}
|
|
263
272
|
return runWindowsClientTrustCommand("PrepareRoot", {
|
|
264
273
|
...options,
|
|
265
274
|
clientRoot,
|
|
@@ -272,6 +281,14 @@ export async function secureWindowsClientReleaseTree(clientRoot, releaseId, opti
|
|
|
272
281
|
if (!/^(?!\.{1,2}$)[A-Za-z0-9.][A-Za-z0-9_.-]{2,191}$/.test(acceptedReleaseId)) {
|
|
273
282
|
throw new HelloLynkCredentialStoreError("client_release_path_invalid");
|
|
274
283
|
}
|
|
284
|
+
if ((options.platform || process.platform) === "darwin") {
|
|
285
|
+
checkDarwinPath(clientRoot, { directory: true, privateMode: true });
|
|
286
|
+
checkDarwinPath(join(clientRoot, "releases"), { directory: true, privateMode: true });
|
|
287
|
+
const release = join(clientRoot, "releases", acceptedReleaseId);
|
|
288
|
+
checkDarwinPath(release, { directory: true });
|
|
289
|
+
await invokeDarwinHelper(options.helperPath || DARWIN_HELPER, [options.verifyOnly ? "verify-tree" : "secure-tree", release]);
|
|
290
|
+
return options.verifyOnly ? "verified" : "secured";
|
|
291
|
+
}
|
|
275
292
|
return runWindowsClientTrustCommand(options.verifyOnly === true ? "VerifyReleaseTree" : "SecureReleaseTree", {
|
|
276
293
|
...options,
|
|
277
294
|
clientRoot,
|
|
@@ -460,24 +477,61 @@ export function createPlatformCredentialStore(options = {}) {
|
|
|
460
477
|
}
|
|
461
478
|
|
|
462
479
|
if (platform === "darwin") {
|
|
463
|
-
const
|
|
464
|
-
homedir(), "Library", "Application Support", "HelloLynk"
|
|
465
|
-
);
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
);
|
|
480
|
+
const clientRoot = options.clientRoot || (options.storeRoot ? windowsClientRootForStore(options.storeRoot)
|
|
481
|
+
: join(homedir(), "Library", "Application Support", "HelloLynk"));
|
|
482
|
+
if (clientRoot !== resolve(clientRoot) || clientRoot === "/") throw new HelloLynkCredentialStoreError("client_root_path_invalid");
|
|
483
|
+
const credentials = join(clientRoot, "credentials");
|
|
484
|
+
const privateKeys = join(clientRoot, "e2ee-private-keys");
|
|
485
|
+
const root = options.storeRoot || credentials;
|
|
486
|
+
const e2eeRoot = options.e2eeStoreRoot || privateKeys;
|
|
487
|
+
if (root !== clientRoot && root !== credentials) throw new HelloLynkCredentialStoreError("credential_store_root_invalid");
|
|
488
|
+
if (e2eeRoot !== privateKeys) throw new HelloLynkCredentialStoreError("e2ee_private_key_store_root_invalid");
|
|
489
|
+
function entry(location, section, profile, operation, value) {
|
|
490
|
+
const slot = section + ":" + assertCredentialProfileId(profile);
|
|
491
|
+
return withDarwinVault({ root: location, helperPath: options.helperPath, readOnly: operation === "load", allowMissing:true }, data => {
|
|
492
|
+
if (operation === "save") data[slot] = value;
|
|
493
|
+
if (operation === "remove") delete data[slot];
|
|
494
|
+
return data[slot] ?? null;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
471
497
|
return {
|
|
472
498
|
platform,
|
|
473
|
-
async save(profile, credential) {
|
|
474
|
-
await invoke("Store", assertCredentialProfileId(profile), assertCredential(credential));
|
|
475
|
-
},
|
|
499
|
+
async save(profile, credential) { await entry(root, "device", profile, "save", assertCredential(credential)); },
|
|
476
500
|
async load(profile) {
|
|
477
|
-
|
|
501
|
+
const value = await entry(root, "device", profile, "load");
|
|
502
|
+
if (value === null) throw new HelloLynkCredentialStoreError("device_credential_missing");
|
|
503
|
+
return assertCredential(value);
|
|
478
504
|
},
|
|
479
|
-
async remove(profile) {
|
|
480
|
-
|
|
505
|
+
async remove(profile) { await entry(root, "device", profile, "remove"); },
|
|
506
|
+
runtimeSessions: {
|
|
507
|
+
async save(profile, session) { await entry(root, "session", profile, "save", assertRuntimeSession(session)); },
|
|
508
|
+
async load(profile) {
|
|
509
|
+
const value = await entry(root, "session", profile, "load");
|
|
510
|
+
return value === null ? null : assertRuntimeSession(value);
|
|
511
|
+
},
|
|
512
|
+
async remove(profile) { await entry(root, "session", profile, "remove"); },
|
|
513
|
+
async withLock(profile, operation) {
|
|
514
|
+
const id = assertCredentialProfileId(profile);
|
|
515
|
+
if (typeof operation !== "function") throw new HelloLynkCredentialStoreError("runtime_session_lock_operation_invalid");
|
|
516
|
+
checkDarwinPath(root, { directory:true, create:true, privateMode:true });
|
|
517
|
+
return withRuntimeSessionFileLock(root, id, operation, options.runtimeSessionLockDependencies, options.timeoutMs);
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
directTextPrivateKeys: {
|
|
521
|
+
async save(profile, material) { await entry(e2eeRoot, "keys", profile, "save", encodeDirectTextPrivateKeyMaterial(material)); },
|
|
522
|
+
async load(profile) {
|
|
523
|
+
const value = await entry(e2eeRoot, "keys", profile, "load");
|
|
524
|
+
return value === null ? null : decodeDirectTextPrivateKeyMaterial(value);
|
|
525
|
+
},
|
|
526
|
+
async remove(profile) { await entry(e2eeRoot, "keys", profile, "remove"); },
|
|
527
|
+
async preload(profiles) {
|
|
528
|
+
const ids = [...new Set((Array.isArray(profiles) ? profiles : []).map(assertCredentialProfileId))];
|
|
529
|
+
if (!ids.length) return;
|
|
530
|
+
if (ids.length > MAX_E2EE_BATCH_PROFILES) throw new HelloLynkCredentialStoreError("e2ee_private_key_batch_invalid");
|
|
531
|
+
await withDarwinVault({root:e2eeRoot, helperPath:options.helperPath, readOnly:true, allowMissing:true}, data => {
|
|
532
|
+
for (const id of ids) if (data["keys:" + id] !== undefined) decodeDirectTextPrivateKeyMaterial(data["keys:" + id]);
|
|
533
|
+
});
|
|
534
|
+
},
|
|
481
535
|
},
|
|
482
536
|
};
|
|
483
537
|
}
|