@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.99
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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +86 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +160 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { contactRequestIntroReplayKey } from "./hellolynk-contact-request-e2ee.mjs";
|
|
5
|
+
import {
|
|
6
|
+
DIRECT_TEXT_E2EE_ALLOWED_INTENT_SOURCES,
|
|
7
|
+
DIRECT_TEXT_E2EE_AUTHORIZATION_SCOPE,
|
|
8
|
+
} from "./hellolynk-direct-text-e2ee.mjs";
|
|
9
|
+
import { assertLocalDataProtectionAccess } from "./hellolynk-local-data-protection.mjs";
|
|
10
|
+
import {
|
|
11
|
+
createOutgoingConversationIntent,
|
|
12
|
+
deriveInstanceConversationBinding,
|
|
13
|
+
instanceConversationKey,
|
|
14
|
+
normalizeInstanceConversationBinding,
|
|
15
|
+
} from "./hellolynk-instance-conversation.mjs";
|
|
16
|
+
|
|
17
|
+
export const HELLOLYNK_NEUTRAL_HOST_RECOVERY_CONTRACT = "hellolynk-neutral-host-recovery-v1";
|
|
18
|
+
|
|
19
|
+
const MAX_JOURNAL_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
const queues = new Map();
|
|
21
|
+
|
|
22
|
+
export function createNeutralHostStoreAdapters(options = {}) {
|
|
23
|
+
const messageStore = requireMethods(options.messageStore, [
|
|
24
|
+
"addIncoming", "addOutgoing", "claimOutgoingForAttempt", "updateOutgoing", "list", "getOutgoing",
|
|
25
|
+
], "neutral_host_message_store_invalid");
|
|
26
|
+
const contactRequestStore = requireMethods(options.contactRequestStore, [
|
|
27
|
+
"persistIncoming", "listForHuman",
|
|
28
|
+
], "neutral_host_contact_store_invalid");
|
|
29
|
+
const principalId = safeId(options.principalId, "neutral_host_store_principal_invalid");
|
|
30
|
+
const journal = createRecoveryJournal({
|
|
31
|
+
journalPath: options.journalPath,
|
|
32
|
+
principalId,
|
|
33
|
+
profileId: options.profileId,
|
|
34
|
+
dataProtector: options.dataProtector,
|
|
35
|
+
maximumBytes: options.recoveryJournalMaximumBytes,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function persistIncomingMessageRecoverable(input = {}) {
|
|
39
|
+
validatePersistenceInput(input, false);
|
|
40
|
+
const event = input.event;
|
|
41
|
+
if ((input.replyToClientMessageId ?? null) !== (event.e2eeEnvelope?.protected?.replyToClientMessageId ?? null)) {
|
|
42
|
+
throw failure("neutral_host_persistence_binding_mismatch");
|
|
43
|
+
}
|
|
44
|
+
const conversationBinding = normalizeInstanceConversationBinding(input.conversationBinding, { principalId });
|
|
45
|
+
if (conversationBinding) {
|
|
46
|
+
// Opening the envelope is the caller's responsibility. Here the already
|
|
47
|
+
// verified tuple must also agree with every corresponding event field.
|
|
48
|
+
const eventBinding = deriveInstanceConversationBinding({ ...event, e2eeVersion: 2 }, {
|
|
49
|
+
direction: "incoming", principalId,
|
|
50
|
+
});
|
|
51
|
+
if (instanceConversationKey(conversationBinding) !== instanceConversationKey(eventBinding)
|
|
52
|
+
|| input.senderPrincipalId !== conversationBinding.peerPrincipalId
|
|
53
|
+
|| input.senderDelegateId !== conversationBinding.peerDelegateId) {
|
|
54
|
+
throw failure("neutral_host_persistence_binding_mismatch");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const identity = eventIdentity("message", event, input.envelopeDigest);
|
|
58
|
+
return journal.coordinate(identity, async (current, checkpoint, recovery) => {
|
|
59
|
+
if (current.stage === "committed") {
|
|
60
|
+
const stored = typeof messageStore.getIncoming === "function"
|
|
61
|
+
? await messageStore.getIncoming(current.localRecordId)
|
|
62
|
+
: (await messageStore.list()).find((item) => item.localMessageId === current.localRecordId);
|
|
63
|
+
if (stored?.conversationBinding && (!conversationBinding
|
|
64
|
+
|| instanceConversationKey(stored.conversationBinding) !== instanceConversationKey(conversationBinding))) {
|
|
65
|
+
throw failure("neutral_host_persistence_binding_mismatch");
|
|
66
|
+
}
|
|
67
|
+
if (!stored) throw failure("neutral_host_persistence_record_missing");
|
|
68
|
+
return persistenceReceipt(current, true, stored);
|
|
69
|
+
}
|
|
70
|
+
const stored = await messageStore.addIncoming({
|
|
71
|
+
eventId: event.eventId,
|
|
72
|
+
clientMessageId: event.clientMessageId,
|
|
73
|
+
senderPrincipalId: input.senderPrincipalId,
|
|
74
|
+
senderDelegateId: input.senderDelegateId,
|
|
75
|
+
authorizationScope: input.authorizationScope,
|
|
76
|
+
intentSource: input.intentSource,
|
|
77
|
+
...(input.replyToClientMessageId ? { replyToClientMessageId: input.replyToClientMessageId } : {}),
|
|
78
|
+
...(input.targetConversationId ? { targetConversationId: input.targetConversationId } : {}),
|
|
79
|
+
...(input.friendTarget ? { friendTarget: input.friendTarget } : {}),
|
|
80
|
+
senderHelloLynkId: event.senderHelloLynkId ?? null,
|
|
81
|
+
senderPrincipalDisplayName: event.senderPrincipalDisplayName ?? null,
|
|
82
|
+
senderDelegateDisplayName: event.senderDelegateDisplayName ?? null,
|
|
83
|
+
text: input.text,
|
|
84
|
+
envelopeDigest: input.envelopeDigest,
|
|
85
|
+
senderIdentityKeyId: event.senderIdentityKeyId,
|
|
86
|
+
recipientIdentityKeyId: event.recipientIdentityKeyId,
|
|
87
|
+
conversationBinding,
|
|
88
|
+
});
|
|
89
|
+
const record = await checkpoint({
|
|
90
|
+
stage: "recorded",
|
|
91
|
+
localRecordId: stored.message.localMessageId,
|
|
92
|
+
localRecordDigest: null,
|
|
93
|
+
});
|
|
94
|
+
await input.commitPrekey();
|
|
95
|
+
const committed = await checkpoint({ ...record, stage: "committed" });
|
|
96
|
+
if (recovery.createdHere && stored.duplicate === true) {
|
|
97
|
+
const replaced = await recovery.replaceDuplicate({
|
|
98
|
+
eventId: stored.message.eventId,
|
|
99
|
+
localRecordId: stored.message.localMessageId,
|
|
100
|
+
});
|
|
101
|
+
if (!replaced) await recovery.discard();
|
|
102
|
+
}
|
|
103
|
+
return persistenceReceipt(committed, stored.duplicate === true, stored.message);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const localStore = Object.freeze({
|
|
107
|
+
...messageStore,
|
|
108
|
+
persistIncomingRecoverable: persistIncomingMessageRecoverable,
|
|
109
|
+
// Historical compatibility alias. This is a recoverable ordered journal,
|
|
110
|
+
// not a transaction spanning the business store and encrypted key store.
|
|
111
|
+
persistIncomingAtomic: persistIncomingMessageRecoverable,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
async function persistIncomingContactRecoverable(input = {}) {
|
|
115
|
+
validatePersistenceInput(input, true);
|
|
116
|
+
const event = input.event;
|
|
117
|
+
const identity = eventIdentity("contact_request", event, input.envelopeDigest);
|
|
118
|
+
return journal.coordinate(identity, async (current, checkpoint, recovery) => {
|
|
119
|
+
if (current.stage === "committed") return contactPersistenceReceipt(current, event, principalId, true);
|
|
120
|
+
const stored = await contactRequestStore.persistIncoming({
|
|
121
|
+
eventId: event.eventId,
|
|
122
|
+
attemptId: event.attemptId,
|
|
123
|
+
clientRequestId: event.clientRequestId,
|
|
124
|
+
connectionId: event.targetConnectionId,
|
|
125
|
+
claimId: event.claimId,
|
|
126
|
+
replayKey: contactRequestIntroReplayKey({
|
|
127
|
+
attemptId: event.attemptId,
|
|
128
|
+
clientRequestId: event.clientRequestId,
|
|
129
|
+
requestDigest: event.requestDigest,
|
|
130
|
+
introDigest: event.introDigest,
|
|
131
|
+
introWeight: event.introWeight,
|
|
132
|
+
senderPrincipalId: event.senderPrincipalId,
|
|
133
|
+
senderHelloLynkId: event.senderHelloLynkId,
|
|
134
|
+
senderDeviceId: event.senderDeviceId,
|
|
135
|
+
senderIdentityKeyId: event.senderIdentityKeyId,
|
|
136
|
+
recipientPrincipalId: event.recipientPrincipalId,
|
|
137
|
+
recipientHelloLynkId: event.recipientHelloLynkId,
|
|
138
|
+
recipientDeviceId: event.recipientDeviceId,
|
|
139
|
+
recipientIdentityKeyId: event.recipientIdentityKeyId,
|
|
140
|
+
recipientPreKeyId: event.recipientPreKeyId,
|
|
141
|
+
authorizationScope: "contact.request.send",
|
|
142
|
+
intentSource: "hellolynk_host_explicit_human_ui",
|
|
143
|
+
messageType: "contact_request_intro",
|
|
144
|
+
createdAt: event.createdAt,
|
|
145
|
+
expiresAt: event.expiresAt,
|
|
146
|
+
}),
|
|
147
|
+
envelopeDigest: input.envelopeDigest,
|
|
148
|
+
requestDigest: event.requestDigest,
|
|
149
|
+
introDigest: event.introDigest,
|
|
150
|
+
introDigestSalt: input.introDigestSalt,
|
|
151
|
+
introWeight: event.introWeight,
|
|
152
|
+
introText: input.text,
|
|
153
|
+
senderPrincipalId: event.senderPrincipalId,
|
|
154
|
+
senderHelloLynkId: event.senderHelloLynkId,
|
|
155
|
+
senderPrincipalDisplayName: event.senderPrincipalDisplayName ?? null,
|
|
156
|
+
senderDeviceId: event.senderDeviceId,
|
|
157
|
+
senderIdentityKeyId: event.senderIdentityKeyId,
|
|
158
|
+
recipientPrincipalId: event.recipientPrincipalId,
|
|
159
|
+
recipientHelloLynkId: event.recipientHelloLynkId,
|
|
160
|
+
recipientDeviceId: event.recipientDeviceId,
|
|
161
|
+
recipientIdentityKeyId: event.recipientIdentityKeyId,
|
|
162
|
+
recipientPreKeyId: event.recipientPreKeyId,
|
|
163
|
+
createdAt: event.createdAt,
|
|
164
|
+
expiresAt: event.expiresAt,
|
|
165
|
+
relayedAt: event.relayedAt,
|
|
166
|
+
ackDeadlineAt: event.ackDeadlineAt,
|
|
167
|
+
});
|
|
168
|
+
const record = await checkpoint({
|
|
169
|
+
stage: "recorded",
|
|
170
|
+
localRecordId: stored.localRecordId,
|
|
171
|
+
localRecordDigest: stored.localRecordDigest,
|
|
172
|
+
});
|
|
173
|
+
await input.commitPrekey();
|
|
174
|
+
const committed = await checkpoint({ ...record, stage: "committed" });
|
|
175
|
+
if (recovery.createdHere && stored.duplicate === true) {
|
|
176
|
+
const replaced = await recovery.replaceDuplicate({
|
|
177
|
+
eventId: stored.eventId,
|
|
178
|
+
localRecordId: stored.localRecordId,
|
|
179
|
+
});
|
|
180
|
+
if (!replaced) await recovery.discard();
|
|
181
|
+
}
|
|
182
|
+
return contactPersistenceReceipt(committed, event, principalId, stored.duplicate === true);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const contactStore = Object.freeze({
|
|
186
|
+
...contactRequestStore,
|
|
187
|
+
persistIncomingRecoverable: persistIncomingContactRecoverable,
|
|
188
|
+
// Historical compatibility alias; see the message adapter note above.
|
|
189
|
+
persistIncomingAtomic: persistIncomingContactRecoverable,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const outbox = Object.freeze({
|
|
193
|
+
...messageStore,
|
|
194
|
+
async persistBeforeEncryption(input = {}) {
|
|
195
|
+
const created = await messageStore.addOutgoing({
|
|
196
|
+
recipientHelloLynkId: input.recipientHelloLynkId,
|
|
197
|
+
clientMessageId: input.clientMessageId,
|
|
198
|
+
text: input.text,
|
|
199
|
+
senderPrincipalId: input.senderPrincipalId,
|
|
200
|
+
senderDelegateId: input.senderDelegateId,
|
|
201
|
+
e2ee: null,
|
|
202
|
+
conversationIntent: createOutgoingConversationIntent(input),
|
|
203
|
+
});
|
|
204
|
+
return messageStore.claimOutgoingForAttempt(created.clientMessageId, {
|
|
205
|
+
senderPrincipalId: input.senderPrincipalId,
|
|
206
|
+
senderDelegateId: input.senderDelegateId,
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
async markUnknown(input = {}) {
|
|
210
|
+
return messageStore.updateOutgoing(input.clientMessageId, {
|
|
211
|
+
status: "delivery_state_unknown",
|
|
212
|
+
lastErrorCode: String(input.errorCode || "neutral_host_delivery_unknown"),
|
|
213
|
+
incrementAttempts: false,
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
async markResult(input = {}) {
|
|
217
|
+
const status = String(input.result?.status || "delivery_state_unknown");
|
|
218
|
+
return messageStore.updateOutgoing(input.clientMessageId, {
|
|
219
|
+
status,
|
|
220
|
+
lastErrorCode: input.result?.errorCode ?? null,
|
|
221
|
+
incrementAttempts: false,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
async freezeUnknown() {
|
|
225
|
+
// Unknown is already a durable status. Startup only reports its count;
|
|
226
|
+
// loading every message body here delays opening the Host listener.
|
|
227
|
+
if (typeof messageStore.getMessageCounts === "function") {
|
|
228
|
+
const states = (await messageStore.getMessageCounts()).outbox.states;
|
|
229
|
+
return Object.freeze({ frozen: (states.delivery_state_unknown ?? 0) + (states.delivery_state_unconfirmed ?? 0) });
|
|
230
|
+
}
|
|
231
|
+
const records = await messageStore.list();
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
frozen: records.filter((item) => item.direction === "outgoing"
|
|
234
|
+
&& ["delivery_state_unknown", "delivery_state_unconfirmed"].includes(item.status)).length,
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
get: (clientMessageId) => messageStore.getOutgoing(clientMessageId),
|
|
238
|
+
retry: async () => { throw failure("neutral_host_native_human_broker_unavailable"); },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return Object.freeze({ localStore, contactStore, outbox, journal });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function createRecoveryJournal(options = {}) {
|
|
245
|
+
const journalPath = path.resolve(String(options.journalPath || ""));
|
|
246
|
+
const principalId = safeId(options.principalId, "neutral_host_store_principal_invalid");
|
|
247
|
+
if (!options.journalPath) throw failure("neutral_host_recovery_journal_path_required");
|
|
248
|
+
const protection = normalizeRecoveryDataProtection(options.dataProtector, options.profileId);
|
|
249
|
+
const maximumBytes = normalizeRecoveryJournalMaximumBytes(options.maximumBytes);
|
|
250
|
+
|
|
251
|
+
async function coordinate(identity, operation) {
|
|
252
|
+
return queued(journalPath, async () => {
|
|
253
|
+
const state = await readJournal(journalPath, principalId, protection, maximumBytes);
|
|
254
|
+
let current = state.operations.find((item) => item.key === identity.key);
|
|
255
|
+
if (current && current.eventDigest !== identity.eventDigest) {
|
|
256
|
+
throw failure("neutral_host_recovery_event_conflict");
|
|
257
|
+
}
|
|
258
|
+
const reclaimed = reclaimCompletedOperations(state, identity.key);
|
|
259
|
+
current = state.operations.find((item) => item.key === identity.key);
|
|
260
|
+
let createdHere = false;
|
|
261
|
+
if (!current) {
|
|
262
|
+
current = {
|
|
263
|
+
key: identity.key,
|
|
264
|
+
kind: identity.kind,
|
|
265
|
+
eventDigest: identity.eventDigest,
|
|
266
|
+
stage: "intent",
|
|
267
|
+
localRecordId: null,
|
|
268
|
+
localRecordDigest: null,
|
|
269
|
+
};
|
|
270
|
+
state.operations.push(current);
|
|
271
|
+
createdHere = true;
|
|
272
|
+
await atomicWriteJson(journalPath, state, protection, maximumBytes);
|
|
273
|
+
} else if (reclaimed) {
|
|
274
|
+
await atomicWriteJson(journalPath, state, protection, maximumBytes);
|
|
275
|
+
}
|
|
276
|
+
const checkpoint = async (patch) => {
|
|
277
|
+
current = { ...current, ...patch };
|
|
278
|
+
state.operations[state.operations.findIndex((item) => item.key === current.key)] = current;
|
|
279
|
+
await atomicWriteJson(journalPath, state, protection, maximumBytes);
|
|
280
|
+
return current;
|
|
281
|
+
};
|
|
282
|
+
const discard = async () => {
|
|
283
|
+
const index = state.operations.findIndex((item) => item.key === current.key);
|
|
284
|
+
if (index < 0) throw failure("neutral_host_recovery_operation_missing");
|
|
285
|
+
state.operations.splice(index, 1);
|
|
286
|
+
await atomicWriteJson(journalPath, state, protection, maximumBytes);
|
|
287
|
+
};
|
|
288
|
+
const replaceDuplicate = async (input = {}) => {
|
|
289
|
+
const sourceEventId = safeId(input.eventId, "neutral_host_recovery_duplicate_event_invalid");
|
|
290
|
+
const sourceLocalRecordId = safeId(
|
|
291
|
+
input.localRecordId,
|
|
292
|
+
"neutral_host_recovery_duplicate_record_invalid",
|
|
293
|
+
);
|
|
294
|
+
const index = state.operations.findIndex((item) => (
|
|
295
|
+
item.key === `${identity.kind}:${sourceEventId}`
|
|
296
|
+
&& item.key !== current.key
|
|
297
|
+
));
|
|
298
|
+
if (index < 0) return false;
|
|
299
|
+
const previous = state.operations[index];
|
|
300
|
+
if (previous.localRecordId != null && previous.localRecordId !== sourceLocalRecordId) {
|
|
301
|
+
throw failure("neutral_host_recovery_duplicate_record_conflict");
|
|
302
|
+
}
|
|
303
|
+
state.operations.splice(index, 1);
|
|
304
|
+
await atomicWriteJson(journalPath, state, protection, maximumBytes);
|
|
305
|
+
return true;
|
|
306
|
+
};
|
|
307
|
+
return operation(structuredClone(current), checkpoint, Object.freeze({
|
|
308
|
+
createdHere,
|
|
309
|
+
discard,
|
|
310
|
+
replaceDuplicate,
|
|
311
|
+
}));
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function snapshot() {
|
|
316
|
+
return queued(
|
|
317
|
+
journalPath,
|
|
318
|
+
async () => structuredClone(await readJournal(journalPath, principalId, protection, maximumBytes)),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return Object.freeze({ path: journalPath, coordinate, snapshot });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validatePersistenceInput(input, contact) {
|
|
326
|
+
requireRecord(input, "neutral_host_persistence_input_invalid");
|
|
327
|
+
if (!input.event || typeof input.text !== "string" || typeof input.commitPrekey !== "function") {
|
|
328
|
+
throw failure("neutral_host_persistence_input_invalid");
|
|
329
|
+
}
|
|
330
|
+
if (input.modelVisibility !== "none" || Boolean(input.humanOnly) !== contact) {
|
|
331
|
+
throw failure("neutral_host_persistence_visibility_invalid");
|
|
332
|
+
}
|
|
333
|
+
if (!contact && (
|
|
334
|
+
!safeId(input.senderPrincipalId, "neutral_host_persistence_provenance_invalid")
|
|
335
|
+
|| !safeId(input.senderDelegateId, "neutral_host_persistence_provenance_invalid")
|
|
336
|
+
|| input.authorizationScope !== DIRECT_TEXT_E2EE_AUTHORIZATION_SCOPE
|
|
337
|
+
|| !DIRECT_TEXT_E2EE_ALLOWED_INTENT_SOURCES.includes(input.intentSource)
|
|
338
|
+
)) throw failure("neutral_host_persistence_provenance_invalid");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function eventIdentity(kind, event, envelopeDigest) {
|
|
342
|
+
requireRecord(event, "neutral_host_persistence_event_invalid");
|
|
343
|
+
const eventId = safeId(event.eventId, "neutral_host_persistence_event_invalid");
|
|
344
|
+
const digest = safeDigest(envelopeDigest, "neutral_host_persistence_event_invalid");
|
|
345
|
+
const immutableFields = kind === "message" ? [
|
|
346
|
+
"eventId", "clientMessageId", "senderPrincipalId", "senderHelloLynkId", "senderAgentId",
|
|
347
|
+
"senderDelegateId", "senderDelegateAuthorizationVersion", "senderHostId", "senderDeviceId",
|
|
348
|
+
"senderIdentityKeyId", "recipientPrincipalId", "recipientHelloLynkId", "recipientDelegateId",
|
|
349
|
+
"recipientDelegateAuthorizationVersion", "recipientHostId", "recipientRouteVersion",
|
|
350
|
+
"recipientDeviceId", "recipientIdentityKeyId", "recipientPreKeyId", "claimId", "routeKind",
|
|
351
|
+
] : [
|
|
352
|
+
"eventId", "attemptId", "clientRequestId", "requestDigest", "introDigest",
|
|
353
|
+
"introWeight", "senderPrincipalId", "senderHelloLynkId", "senderDeviceId", "senderIdentityKeyId",
|
|
354
|
+
"recipientPrincipalId", "recipientHelloLynkId", "recipientDeviceId", "recipientIdentityKeyId",
|
|
355
|
+
"recipientPreKeyId", "claimId", "createdAt", "expiresAt",
|
|
356
|
+
];
|
|
357
|
+
const immutable = Object.fromEntries(immutableFields.map((field) => [field, event[field] ?? null]));
|
|
358
|
+
return Object.freeze({
|
|
359
|
+
key: `${kind}:${eventId}`,
|
|
360
|
+
kind,
|
|
361
|
+
eventDigest: crypto.createHash("sha256")
|
|
362
|
+
.update(JSON.stringify({ kind, envelopeDigest: digest, immutable }), "utf8")
|
|
363
|
+
.digest("hex"),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function persistenceReceipt(item, duplicate, message) {
|
|
368
|
+
return Object.freeze({ localRecordId: item.localRecordId, duplicate,
|
|
369
|
+
...(message?.replyTargetDeleted === true ? { replyTargetDeleted: true } : {}) });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function contactPersistenceReceipt(item, event, principalId, duplicate) {
|
|
373
|
+
const proofInput = {
|
|
374
|
+
attemptId: event.attemptId,
|
|
375
|
+
eventId: event.eventId,
|
|
376
|
+
clientRequestId: event.clientRequestId,
|
|
377
|
+
recipientPrincipalId: principalId,
|
|
378
|
+
recipientEndpointId: event.recipientDeviceId,
|
|
379
|
+
envelopeDigest: event.envelopeDigest,
|
|
380
|
+
claimId: event.claimId,
|
|
381
|
+
localRecordDigest: item.localRecordDigest,
|
|
382
|
+
};
|
|
383
|
+
return Object.freeze({
|
|
384
|
+
localRecordId: item.localRecordId,
|
|
385
|
+
localRecordDigest: item.localRecordDigest,
|
|
386
|
+
ackProofDigest: crypto.createHash("sha256")
|
|
387
|
+
.update(`hellolynk/contact-request/ack-proof/v1\0${JSON.stringify(proofInput)}`, "utf8")
|
|
388
|
+
.digest("hex"),
|
|
389
|
+
duplicate,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function normalizeRecoveryDataProtection(protector, profileId) {
|
|
394
|
+
if (protector == null) return null;
|
|
395
|
+
if (["sealJson", "openJson"].some((method) => typeof protector?.[method] !== "function")) {
|
|
396
|
+
throw failure("neutral_host_recovery_data_protector_invalid");
|
|
397
|
+
}
|
|
398
|
+
const acceptedProfileId = safeId(profileId, "neutral_host_recovery_profile_invalid");
|
|
399
|
+
return Object.freeze({
|
|
400
|
+
protector,
|
|
401
|
+
context: Object.freeze({
|
|
402
|
+
profileId: acceptedProfileId,
|
|
403
|
+
assetKind: "neutral-host-recovery",
|
|
404
|
+
logicalPath: "neutral-host-recovery-v1.json",
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normalizeRecoveryJournalMaximumBytes(value) {
|
|
410
|
+
if (value == null) return MAX_JOURNAL_BYTES;
|
|
411
|
+
if (!Number.isSafeInteger(value) || value < 1024 || value > MAX_JOURNAL_BYTES) {
|
|
412
|
+
throw failure("neutral_host_recovery_journal_maximum_invalid");
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function reclaimCompletedOperations(state, currentKey) {
|
|
418
|
+
// The business record and prekey commit are already durable. Replay evidence
|
|
419
|
+
// lives in their stores. Retain unfinished work and the current replay entry.
|
|
420
|
+
const retained = state.operations.filter((item) => item?.stage !== "committed" || item.key === currentKey);
|
|
421
|
+
const removed = state.operations.length - retained.length;
|
|
422
|
+
if (removed) state.operations = retained;
|
|
423
|
+
return removed;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function readJournal(journalPath, principalId, protection = null, maximumBytes = MAX_JOURNAL_BYTES) {
|
|
427
|
+
let body;
|
|
428
|
+
let handle;
|
|
429
|
+
const physicalMaximum = protection ? maximumBytes * 2 : maximumBytes;
|
|
430
|
+
try {
|
|
431
|
+
await assertRecoveryJournalAccess(journalPath, protection, { create: false });
|
|
432
|
+
const metadata = await fs.lstat(journalPath);
|
|
433
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > physicalMaximum) {
|
|
434
|
+
throw failure("neutral_host_recovery_journal_invalid");
|
|
435
|
+
}
|
|
436
|
+
handle = await fs.open(journalPath, "r");
|
|
437
|
+
const openedMetadata = await handle.stat();
|
|
438
|
+
const windowsUnreportedDevice = process.platform === "win32"
|
|
439
|
+
&& (metadata.dev === 0 || openedMetadata.dev === 0);
|
|
440
|
+
if (!openedMetadata.isFile() || openedMetadata.size > physicalMaximum
|
|
441
|
+
|| (windowsUnreportedDevice && metadata.ino !== openedMetadata.ino)
|
|
442
|
+
|| (Number.isSafeInteger(metadata.dev) && Number.isSafeInteger(metadata.ino)
|
|
443
|
+
&& Number.isSafeInteger(openedMetadata.dev) && Number.isSafeInteger(openedMetadata.ino)
|
|
444
|
+
&& (metadata.ino !== openedMetadata.ino
|
|
445
|
+
|| (metadata.dev !== openedMetadata.dev
|
|
446
|
+
&& !windowsUnreportedDevice)))) {
|
|
447
|
+
throw failure("neutral_host_recovery_journal_invalid");
|
|
448
|
+
}
|
|
449
|
+
body = await handle.readFile("utf8");
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error?.code === "ENOENT") {
|
|
452
|
+
return { contract: HELLOLYNK_NEUTRAL_HOST_RECOVERY_CONTRACT, principalId, operations: [] };
|
|
453
|
+
}
|
|
454
|
+
throw error;
|
|
455
|
+
} finally {
|
|
456
|
+
await handle?.close().catch(() => {});
|
|
457
|
+
}
|
|
458
|
+
let value;
|
|
459
|
+
try {
|
|
460
|
+
const stored = JSON.parse(body);
|
|
461
|
+
value = protection
|
|
462
|
+
? await protection.protector.openJson(stored, protection.context)
|
|
463
|
+
: stored;
|
|
464
|
+
if (serializedJournalBytes(value) > maximumBytes) {
|
|
465
|
+
throw failure("neutral_host_recovery_journal_invalid");
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
throw failure("neutral_host_recovery_journal_invalid");
|
|
469
|
+
}
|
|
470
|
+
if (value?.contract !== HELLOLYNK_NEUTRAL_HOST_RECOVERY_CONTRACT
|
|
471
|
+
|| value?.principalId !== principalId || !Array.isArray(value.operations)) {
|
|
472
|
+
throw failure("neutral_host_recovery_journal_invalid");
|
|
473
|
+
}
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function atomicWriteJson(target, value, protection = null, maximumBytes = MAX_JOURNAL_BYTES) {
|
|
478
|
+
const directory = path.dirname(target);
|
|
479
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
480
|
+
await assertRecoveryJournalAccess(target, protection, { create: false });
|
|
481
|
+
const temporary = `${target}.${crypto.randomUUID()}.tmp`;
|
|
482
|
+
if (serializedJournalBytes(value) > maximumBytes) {
|
|
483
|
+
throw failure("neutral_host_recovery_journal_full");
|
|
484
|
+
}
|
|
485
|
+
const stored = protection
|
|
486
|
+
? await protection.protector.sealJson(value, protection.context)
|
|
487
|
+
: value;
|
|
488
|
+
const body = `${JSON.stringify(stored, null, 2)}\n`;
|
|
489
|
+
const physicalMaximum = protection ? maximumBytes * 2 : maximumBytes;
|
|
490
|
+
if (Buffer.byteLength(body, "utf8") > physicalMaximum) throw failure("neutral_host_recovery_journal_full");
|
|
491
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
492
|
+
try { await handle.writeFile(body, "utf8"); await handle.sync(); } finally { await handle.close(); }
|
|
493
|
+
try {
|
|
494
|
+
await assertRecoveryJournalAccess(target, protection, { create: false });
|
|
495
|
+
await fs.rename(temporary, target);
|
|
496
|
+
await assertRecoveryJournalAccess(target, protection, { create: false });
|
|
497
|
+
} catch (error) { await fs.rm(temporary, { force: true }); throw error; }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function serializedJournalBytes(value) {
|
|
501
|
+
return Buffer.byteLength(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function assertRecoveryJournalAccess(target, protection, options = {}) {
|
|
505
|
+
const root = path.dirname(path.resolve(target));
|
|
506
|
+
if (options.create === true) await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
507
|
+
const parsed = path.parse(root);
|
|
508
|
+
let cursor = parsed.root;
|
|
509
|
+
for (const segment of path.relative(parsed.root, root).split(path.sep).filter(Boolean)) {
|
|
510
|
+
cursor = path.join(cursor, segment);
|
|
511
|
+
const metadata = await fs.lstat(cursor);
|
|
512
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
513
|
+
throw failure("neutral_host_recovery_journal_directory_invalid");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
await assertLocalDataProtectionAccess(root, protection != null);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function queued(key, operation) {
|
|
520
|
+
const previous = queues.get(key) || Promise.resolve();
|
|
521
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
522
|
+
queues.set(key, current);
|
|
523
|
+
try { return await current; } finally { if (queues.get(key) === current) queues.delete(key); }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function requireMethods(value, methods, code) {
|
|
527
|
+
if (!value || methods.some((method) => typeof value[method] !== "function")) throw failure(code);
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function requireRecord(value, code) {
|
|
532
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw failure(code);
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function safeId(value, code) {
|
|
537
|
+
const normalized = String(value || "");
|
|
538
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,199}$/.test(normalized)) throw failure(code);
|
|
539
|
+
return normalized;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function safeDigest(value, code) {
|
|
543
|
+
const normalized = String(value || "");
|
|
544
|
+
if (!/^[a-f0-9]{64}$/.test(normalized)) throw failure(code);
|
|
545
|
+
return normalized;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function failure(code) { const error = new Error(code); error.code = code; return error; }
|