@private.me/xbind 3.0.0 → 3.0.2
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 +55 -7
- package/dist-standalone/_deps/mldsa-wasm/dist/mldsa.js +1920 -1
- package/dist-standalone/_deps/shared/cjs/errors.js +729 -1
- package/dist-standalone/_deps/shared/cjs/index.js +463 -1
- package/dist-standalone/_deps/shared/cjs/types.js +315 -1
- package/dist-standalone/_deps/shared/errors.js +244 -1
- package/dist-standalone/_deps/shared/index.js +72 -1
- package/dist-standalone/_deps/shared/types.js +86 -1
- package/dist-standalone/_deps/ux-helpers/cjs/errors.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/index.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/pagination.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/progress.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/search.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/types.js +1 -1
- package/dist-standalone/_deps/ux-helpers/errors.js +1 -1
- package/dist-standalone/_deps/ux-helpers/index.js +1 -1
- package/dist-standalone/_deps/ux-helpers/pagination.js +1 -1
- package/dist-standalone/_deps/ux-helpers/progress.js +1 -1
- package/dist-standalone/_deps/ux-helpers/search.js +1 -1
- package/dist-standalone/_deps/xchange/auto-accept.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/auto-accept.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/errors.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/index.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/invite-client.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/lazy-init.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/trust-integration.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/xchange.js +1 -1
- package/dist-standalone/_deps/xchange/errors.js +1 -1
- package/dist-standalone/_deps/xchange/index.js +1 -1
- package/dist-standalone/_deps/xchange/invite-client.js +1 -1
- package/dist-standalone/_deps/xchange/lazy-init.js +1 -1
- package/dist-standalone/_deps/xchange/trust-integration.js +1 -1
- package/dist-standalone/_deps/xchange/xchange.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/discovery.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/errors.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/index.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/registry.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/schema.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/types.js +1 -1
- package/dist-standalone/_deps/xregistry/discovery.js +1 -1
- package/dist-standalone/_deps/xregistry/errors.js +1 -1
- package/dist-standalone/_deps/xregistry/index.js +1 -1
- package/dist-standalone/_deps/xregistry/registry.js +1 -1
- package/dist-standalone/_deps/xregistry/schema.js +1 -1
- package/dist-standalone/_deps/xregistry/types.js +1 -1
- package/dist-standalone/agent-call.js +659 -1
- package/dist-standalone/agent-sdk.js +328 -1
- package/dist-standalone/agent.js +1800 -1
- package/dist-standalone/approval.js +193 -1
- package/dist-standalone/async-iterators.js +382 -1
- package/dist-standalone/auth.js +219 -1
- package/dist-standalone/auto-accept.js +229 -1
- package/dist-standalone/backup-config.js +201 -1
- package/dist-standalone/backup.js +326 -1
- package/dist-standalone/batch-operations.js +388 -1
- package/dist-standalone/cancellation.js +477 -1
- package/dist-standalone/checkpoint.js +186 -1
- package/dist-standalone/circuit-breaker.js +468 -1
- package/dist-standalone/cjs/agent-call.js +701 -1
- package/dist-standalone/cjs/agent-sdk.js +332 -1
- package/dist-standalone/cjs/agent.js +1837 -1
- package/dist-standalone/cjs/approval.js +199 -1
- package/dist-standalone/cjs/async-iterators.js +392 -1
- package/dist-standalone/cjs/auth.js +225 -1
- package/dist-standalone/cjs/auto-accept.js +233 -1
- package/dist-standalone/cjs/backup-config.js +207 -1
- package/dist-standalone/cjs/backup.js +330 -1
- package/dist-standalone/cjs/batch-operations.js +397 -1
- package/dist-standalone/cjs/cancellation.js +490 -1
- package/dist-standalone/cjs/checkpoint.js +193 -1
- package/dist-standalone/cjs/circuit-breaker.js +476 -1
- package/dist-standalone/cjs/cli/init.js +492 -1
- package/dist-standalone/cjs/config-validation.js +522 -1
- package/dist-standalone/cjs/connect.js +312 -1
- package/dist-standalone/cjs/connection-pool.js +506 -1
- package/dist-standalone/cjs/correlation-id.js +339 -1
- package/dist-standalone/cjs/crypto-utils.js +176 -1
- package/dist-standalone/cjs/debug-mode.js +534 -1
- package/dist-standalone/cjs/did-document.js +101 -1
- package/dist-standalone/cjs/did-privateme.js +130 -1
- package/dist-standalone/cjs/did-web.js +201 -1
- package/dist-standalone/cjs/discovery.js +462 -1
- package/dist-standalone/cjs/dual-mode.js +251 -1
- package/dist-standalone/cjs/email-templates.js +313 -1
- package/dist-standalone/cjs/email-transport.js +239 -1
- package/dist-standalone/cjs/envelope.js +538 -1
- package/dist-standalone/cjs/errors.js +913 -1
- package/dist-standalone/cjs/event-emitter.js +461 -1
- package/dist-standalone/cjs/gateway-state.js +55 -1
- package/dist-standalone/cjs/gateway-transport.js +120 -1
- package/dist-standalone/cjs/graceful-degradation.js +403 -1
- package/dist-standalone/cjs/guardrails.js +223 -1
- package/dist-standalone/cjs/health-check.js +336 -1
- package/dist-standalone/cjs/http-compat.js +272 -1
- package/dist-standalone/cjs/http-status-map.js +571 -1
- package/dist-standalone/cjs/identity.js +645 -1
- package/dist-standalone/cjs/index.js +406 -1
- package/dist-standalone/cjs/invitation.js +421 -1
- package/dist-standalone/cjs/invite.js +328 -1
- package/dist-standalone/cjs/key-agreement.js +335 -1
- package/dist-standalone/cjs/lazy-init.js +300 -1
- package/dist-standalone/cjs/logger.js +291 -1
- package/dist-standalone/cjs/mdns-discovery.js +202 -1
- package/dist-standalone/cjs/nonce-store.js +80 -1
- package/dist-standalone/cjs/pairing-manager.js +223 -1
- package/dist-standalone/cjs/plugin-system.js +264 -1
- package/dist-standalone/cjs/plugins/logging.js +168 -1
- package/dist-standalone/cjs/plugins/metrics.js +181 -1
- package/dist-standalone/cjs/plugins/validation.js +302 -1
- package/dist-standalone/cjs/policy.js +320 -1
- package/dist-standalone/cjs/progress-callbacks.js +583 -1
- package/dist-standalone/cjs/redis-nonce-store.js +76 -1
- package/dist-standalone/cjs/registry-middleware.js +50 -1
- package/dist-standalone/cjs/retry-strategies.js +544 -1
- package/dist-standalone/cjs/retry-transport.js +102 -1
- package/dist-standalone/cjs/runtime/browser.js +533 -1
- package/dist-standalone/cjs/runtime/edge.js +526 -1
- package/dist-standalone/cjs/runtime/react-native.js +394 -1
- package/dist-standalone/cjs/security-policy.js +245 -1
- package/dist-standalone/cjs/serialization.js +1040 -1
- package/dist-standalone/cjs/split-channel.js +225 -1
- package/dist-standalone/cjs/subscription-proof.js +230 -1
- package/dist-standalone/cjs/succession.js +148 -1
- package/dist-standalone/cjs/timeouts.js +412 -1
- package/dist-standalone/cjs/trace-context.js +424 -1
- package/dist-standalone/cjs/trace-spans.js +495 -1
- package/dist-standalone/cjs/transport.js +63 -1
- package/dist-standalone/cjs/trust-registry.js +991 -1
- package/dist-standalone/cjs/types/error-response.js +56 -1
- package/dist-standalone/cjs/vault-auth.js +178 -1
- package/dist-standalone/cjs/vault-store-loader.js +194 -1
- package/dist-standalone/cjs/verify.js +25 -1
- package/dist-standalone/cjs/version-info.js +543 -1
- package/dist-standalone/cjs/xfetch.js +340 -1
- package/dist-standalone/cli/init.js +455 -1
- package/dist-standalone/cli/setup.js +514 -1
- package/dist-standalone/cli/types.js +27 -1
- package/dist-standalone/cli/xbind.js +148 -1
- package/dist-standalone/config-validation.js +513 -1
- package/dist-standalone/connect.js +274 -1
- package/dist-standalone/connection-pool.js +500 -1
- package/dist-standalone/correlation-id.js +326 -1
- package/dist-standalone/crypto-utils.js +157 -1
- package/dist-standalone/debug-mode.js +510 -1
- package/dist-standalone/did-document.js +96 -1
- package/dist-standalone/did-privateme.js +121 -1
- package/dist-standalone/did-web.js +196 -1
- package/dist-standalone/discovery.js +458 -1
- package/dist-standalone/dual-mode.js +247 -1
- package/dist-standalone/email-templates.js +309 -1
- package/dist-standalone/email-transport.js +232 -1
- package/dist-standalone/envelope.js +525 -1
- package/dist-standalone/errors.js +896 -1
- package/dist-standalone/event-emitter.js +456 -1
- package/dist-standalone/gateway-state.js +51 -1
- package/dist-standalone/gateway-transport.js +116 -1
- package/dist-standalone/graceful-degradation.js +396 -1
- package/dist-standalone/guardrails.js +216 -1
- package/dist-standalone/health-check.js +332 -1
- package/dist-standalone/http-compat.js +267 -1
- package/dist-standalone/http-status-map.js +561 -1
- package/dist-standalone/identity.js +619 -1
- package/dist-standalone/index.js +78 -1
- package/dist-standalone/invitation.js +415 -1
- package/dist-standalone/invite.js +324 -1
- package/dist-standalone/key-agreement.js +325 -1
- package/dist-standalone/lazy-init.js +295 -1
- package/dist-standalone/logger.js +285 -1
- package/dist-standalone/mdns-discovery.js +195 -1
- package/dist-standalone/nonce-store.js +76 -1
- package/dist-standalone/pairing-manager.js +219 -1
- package/dist-standalone/plugin-system.js +257 -1
- package/dist-standalone/plugins/logging.d.ts +84 -0
- package/dist-standalone/plugins/logging.js +163 -0
- package/dist-standalone/plugins/metrics.d.ts +111 -0
- package/dist-standalone/plugins/metrics.js +176 -0
- package/dist-standalone/plugins/validation.d.ts +104 -0
- package/dist-standalone/plugins/validation.js +297 -0
- package/dist-standalone/policy.js +315 -1
- package/dist-standalone/progress-callbacks.js +576 -1
- package/dist-standalone/redis-nonce-store.js +72 -1
- package/dist-standalone/registry-middleware.js +47 -1
- package/dist-standalone/retry-strategies.js +534 -1
- package/dist-standalone/retry-transport.js +98 -1
- package/dist-standalone/runtime/browser.d.ts +311 -0
- package/dist-standalone/runtime/browser.js +516 -0
- package/dist-standalone/runtime/edge.d.ts +282 -0
- package/dist-standalone/runtime/edge.js +511 -0
- package/dist-standalone/runtime/react-native.d.ts +157 -0
- package/dist-standalone/runtime/react-native.js +383 -0
- package/dist-standalone/security-policy.js +239 -1
- package/dist-standalone/serialization.js +1031 -1
- package/dist-standalone/split-channel.js +219 -1
- package/dist-standalone/subscription-proof.js +224 -1
- package/dist-standalone/succession.js +142 -1
- package/dist-standalone/timeouts.js +398 -1
- package/dist-standalone/trace-context.js +414 -1
- package/dist-standalone/trace-spans.js +488 -1
- package/dist-standalone/transport.js +59 -1
- package/dist-standalone/trust-registry.js +950 -1
- package/dist-standalone/types/error-response.d.ts +209 -0
- package/dist-standalone/types/error-response.js +52 -0
- package/dist-standalone/vault-auth.js +174 -1
- package/dist-standalone/vault-store-loader.js +187 -1
- package/dist-standalone/verify.js +16 -1
- package/dist-standalone/version-info.js +530 -1
- package/dist-standalone/xfetch.js +335 -1
- package/package.json +4 -10
- package/share1.dat +0 -0
- package/dist-standalone/_deps/mldsa-wasm/LICENSE +0 -24
- package/dist-standalone/_deps/mldsa-wasm/package.json +0 -46
- package/dist-standalone/_deps/shared/cjs/package.json +0 -1
- package/dist-standalone/_deps/ux-helpers/cjs/package.json +0 -1
- package/dist-standalone/_deps/xchange/cjs/package.json +0 -1
- package/dist-standalone/_deps/xregistry/cjs/package.json +0 -1
- package/dist-standalone/cjs/package.json +0 -3
- package/dist-standalone/package.json +0 -10
|
@@ -1 +1,324 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module invite
|
|
3
|
+
* Viral invite system for XBind connections.
|
|
4
|
+
*
|
|
5
|
+
* Enables one-click invites:
|
|
6
|
+
* - Generate: `xbind invite payments-service`
|
|
7
|
+
* - Accept: Click link or `xbind accept https://xbind.to/invite/abc123`
|
|
8
|
+
*
|
|
9
|
+
* The viral loop:
|
|
10
|
+
* 1. Service A invites Service B
|
|
11
|
+
* 2. Service B clicks link (< 60 sec setup)
|
|
12
|
+
* 3. Connection established
|
|
13
|
+
* 4. Service B invites Service C
|
|
14
|
+
*/
|
|
15
|
+
import { ok, err } from"./_deps/shared/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* Invite error codes.
|
|
18
|
+
*/
|
|
19
|
+
export var InviteErrorCode;
|
|
20
|
+
(function (InviteErrorCode) {
|
|
21
|
+
InviteErrorCode["INVALID_INVITE"] = "INVITE_INVALID";
|
|
22
|
+
InviteErrorCode["EXPIRED_INVITE"] = "INVITE_EXPIRED";
|
|
23
|
+
InviteErrorCode["INVITE_NOT_FOUND"] = "INVITE_NOT_FOUND";
|
|
24
|
+
InviteErrorCode["NETWORK_ERROR"] = "INVITE_NETWORK_ERROR";
|
|
25
|
+
InviteErrorCode["ALREADY_CONNECTED"] = "INVITE_ALREADY_CONNECTED";
|
|
26
|
+
})(InviteErrorCode || (InviteErrorCode = {}));
|
|
27
|
+
/**
|
|
28
|
+
* Invite service for creating and accepting invites.
|
|
29
|
+
*/
|
|
30
|
+
export class InviteService {
|
|
31
|
+
inviteApiUrl;
|
|
32
|
+
/**
|
|
33
|
+
* Create invite service.
|
|
34
|
+
*
|
|
35
|
+
* @param options - Configuration
|
|
36
|
+
* @param options.inviteApiUrl - Invite API URL (default: https://xbind.to)
|
|
37
|
+
*/
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
this.inviteApiUrl = options.inviteApiUrl || 'https://xbind.to';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create an invite.
|
|
43
|
+
*
|
|
44
|
+
* @param options - Invite options
|
|
45
|
+
* @param options.from - Sender service info
|
|
46
|
+
* @param options.target - Target service (optional)
|
|
47
|
+
* @param options.template - Suggested template (e.g., "payment-processing")
|
|
48
|
+
* @param options.permissions - Custom permissions
|
|
49
|
+
* @param options.expiresIn - Expiration in seconds (default: 7 days)
|
|
50
|
+
* @returns Invite or error
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const inviteService = new InviteService();
|
|
55
|
+
* const invite = await inviteService.create({
|
|
56
|
+
* from: {
|
|
57
|
+
* name: 'billing-service',
|
|
58
|
+
* did: 'did:key:z6Mk...',
|
|
59
|
+
* endpoint: 'https://billing.example.com',
|
|
60
|
+
* publicKey: '...',
|
|
61
|
+
* },
|
|
62
|
+
* target: {
|
|
63
|
+
* name: 'payments-service',
|
|
64
|
+
* },
|
|
65
|
+
* template: 'payment-processing',
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* // Share invite.value.url or invite.value.qr
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
async create(options) {
|
|
72
|
+
const expiresIn = options.expiresIn || 7 * 24 * 60 * 60; // 7 days
|
|
73
|
+
const expires = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(`${this.inviteApiUrl}/invites/create`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
from: {
|
|
82
|
+
name: options.from.name,
|
|
83
|
+
did: options.from.did,
|
|
84
|
+
endpoint: options.from.endpoint,
|
|
85
|
+
publicKey: options.from.publicKey,
|
|
86
|
+
x25519PublicKey: options.from.x25519PublicKey,
|
|
87
|
+
mlKemPublicKey: options.from.mlKemPublicKey,
|
|
88
|
+
},
|
|
89
|
+
to: options.target,
|
|
90
|
+
template: options.template,
|
|
91
|
+
permissions: options.permissions,
|
|
92
|
+
expires,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
return err({
|
|
97
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
98
|
+
message: `Failed to create invite: ${response.status}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
return ok(data);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return err({
|
|
106
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
107
|
+
message: error instanceof Error ? error.message : 'Network error',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Accept an invite.
|
|
113
|
+
*
|
|
114
|
+
* @param options - Accept options
|
|
115
|
+
* @param options.inviteUrl - Invite URL (e.g., https://xbind.to/invite/abc123)
|
|
116
|
+
* @param options.acceptor - Acceptor service info
|
|
117
|
+
* @returns Connection info or error
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* const inviteService = new InviteService();
|
|
122
|
+
* const result = await inviteService.accept({
|
|
123
|
+
* inviteUrl: 'https://xbind.to/invite/abc123',
|
|
124
|
+
* acceptor: {
|
|
125
|
+
* name: 'payments-service',
|
|
126
|
+
* did: 'did:key:z6Mk...',
|
|
127
|
+
* endpoint: 'https://payments.example.com',
|
|
128
|
+
* publicKey: '...',
|
|
129
|
+
* }
|
|
130
|
+
* });
|
|
131
|
+
*
|
|
132
|
+
* if (result.ok) {
|
|
133
|
+
* console.log('Connected!');
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
async accept(options) {
|
|
138
|
+
try {
|
|
139
|
+
// Extract invite ID from URL
|
|
140
|
+
const inviteId = this.extractInviteId(options.inviteUrl);
|
|
141
|
+
if (!inviteId) {
|
|
142
|
+
return err({
|
|
143
|
+
code: InviteErrorCode.INVALID_INVITE,
|
|
144
|
+
message: 'Invalid invite URL',
|
|
145
|
+
hint: 'URL should be like: https://xbind.to/invite/abc123',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Accept invite
|
|
149
|
+
const response = await fetch(`${this.inviteApiUrl}/invites/${inviteId}/accept`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
acceptor: {
|
|
156
|
+
name: options.acceptor.name,
|
|
157
|
+
did: options.acceptor.did,
|
|
158
|
+
endpoint: options.acceptor.endpoint,
|
|
159
|
+
publicKey: options.acceptor.publicKey,
|
|
160
|
+
x25519PublicKey: options.acceptor.x25519PublicKey,
|
|
161
|
+
mlKemPublicKey: options.acceptor.mlKemPublicKey,
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
if (response.status === 404) {
|
|
166
|
+
return err({
|
|
167
|
+
code: InviteErrorCode.INVITE_NOT_FOUND,
|
|
168
|
+
message: 'Invite not found or expired',
|
|
169
|
+
hint: 'The invite may have been revoked or expired',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (response.status === 410) {
|
|
173
|
+
const errorData = await response.json().catch(() => ({}));
|
|
174
|
+
return err({
|
|
175
|
+
code: InviteErrorCode.EXPIRED_INVITE,
|
|
176
|
+
message: errorData.message || 'Invite has expired',
|
|
177
|
+
hint: errorData.hint || 'Ask the sender to create a new invite',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (response.status === 409) {
|
|
181
|
+
return err({
|
|
182
|
+
code: InviteErrorCode.ALREADY_CONNECTED,
|
|
183
|
+
message: 'Services are already connected',
|
|
184
|
+
hint: 'You are already connected to this service',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return err({
|
|
189
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
190
|
+
message: `Failed to accept invite: ${response.status}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
return ok(data);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return err({
|
|
198
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
199
|
+
message: error instanceof Error ? error.message : 'Network error',
|
|
200
|
+
hint: 'Check your network connection and try again',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get invite details without accepting.
|
|
206
|
+
*
|
|
207
|
+
* @param inviteUrl - Invite URL
|
|
208
|
+
* @returns Invite details or error
|
|
209
|
+
*/
|
|
210
|
+
async get(inviteUrl) {
|
|
211
|
+
try {
|
|
212
|
+
const inviteId = this.extractInviteId(inviteUrl);
|
|
213
|
+
if (!inviteId) {
|
|
214
|
+
return err({
|
|
215
|
+
code: InviteErrorCode.INVALID_INVITE,
|
|
216
|
+
message: 'Invalid invite URL',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const response = await fetch(`${this.inviteApiUrl}/invites/${inviteId}`, {
|
|
220
|
+
headers: {
|
|
221
|
+
'Accept': 'application/json',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
if (response.status === 404) {
|
|
225
|
+
return err({
|
|
226
|
+
code: InviteErrorCode.INVITE_NOT_FOUND,
|
|
227
|
+
message: 'Invite not found',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
return err({
|
|
232
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
233
|
+
message: `Failed to get invite: ${response.status}`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const data = await response.json();
|
|
237
|
+
return ok(data);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
return err({
|
|
241
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
242
|
+
message: error instanceof Error ? error.message : 'Network error',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Revoke an invite.
|
|
248
|
+
*
|
|
249
|
+
* @param inviteId - Invite ID to revoke
|
|
250
|
+
* @returns Success or error
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* const inviteService = new InviteService();
|
|
255
|
+
* const result = await inviteService.revoke('inv_abc123');
|
|
256
|
+
*
|
|
257
|
+
* if (result.ok) {
|
|
258
|
+
* console.log('Invite revoked');
|
|
259
|
+
* }
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
async revoke(inviteId) {
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(`${this.inviteApiUrl}/invites/${inviteId}/revoke`, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: {
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
if (response.status === 404) {
|
|
271
|
+
return err({
|
|
272
|
+
code: InviteErrorCode.INVITE_NOT_FOUND,
|
|
273
|
+
message: 'Invite not found',
|
|
274
|
+
hint: 'The invite may have already been revoked or expired',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
return err({
|
|
279
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
280
|
+
message: `Failed to revoke invite: ${response.status}`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
return ok(data);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
return err({
|
|
288
|
+
code: InviteErrorCode.NETWORK_ERROR,
|
|
289
|
+
message: error instanceof Error ? error.message : 'Network error',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Extract invite ID from URL.
|
|
295
|
+
*
|
|
296
|
+
* @param url - Invite URL
|
|
297
|
+
* @returns Invite ID or null
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* extractInviteId('https://xbind.to/invite/abc123') // 'abc123'
|
|
302
|
+
* extractInviteId('https://xbind.to/billing/invite/xyz') // 'xyz'
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
extractInviteId(url) {
|
|
306
|
+
try {
|
|
307
|
+
const parsed = new URL(url);
|
|
308
|
+
const segments = parsed.pathname.split('/').filter(s => s.length > 0);
|
|
309
|
+
// Look for 'invite' segment followed by ID
|
|
310
|
+
const inviteIndex = segments.indexOf('invite');
|
|
311
|
+
if (inviteIndex !== -1 && segments[inviteIndex + 1]) {
|
|
312
|
+
return segments[inviteIndex + 1] ?? null;
|
|
313
|
+
}
|
|
314
|
+
// Fallback: last segment
|
|
315
|
+
if (segments.length > 0) {
|
|
316
|
+
return segments[segments.length - 1] ?? null;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -1 +1,325 @@
|
|
|
1
|
-
import{ok,err}from"./_deps/shared/index.js";
|
|
1
|
+
import { ok, err } from"./_deps/shared/index.js";
|
|
2
|
+
import { createMlKem768 } from 'mlkem';
|
|
3
|
+
/* -- Constants -- */
|
|
4
|
+
const X25519_RAW_KEY_BYTES = 32;
|
|
5
|
+
const AES_KEY_BITS = 256;
|
|
6
|
+
/* -- Helpers -- */
|
|
7
|
+
/** Copy Uint8Array to fresh ArrayBuffer. */
|
|
8
|
+
function toArrayBuffer(data) {
|
|
9
|
+
const buf = new ArrayBuffer(data.byteLength);
|
|
10
|
+
new Uint8Array(buf).set(data);
|
|
11
|
+
return buf;
|
|
12
|
+
}
|
|
13
|
+
/* -- Key Generation -- */
|
|
14
|
+
/**
|
|
15
|
+
* Generate an ephemeral X25519 key pair for ECDH key agreement.
|
|
16
|
+
*
|
|
17
|
+
* Uses Web Crypto API to generate a fresh X25519 key pair.
|
|
18
|
+
* The key pair is ephemeral -- it should be used for a single
|
|
19
|
+
* key agreement and then discarded.
|
|
20
|
+
*
|
|
21
|
+
* @returns Result containing the ephemeral key pair or error.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { generateEphemeralKeyPair } from '@private.me/xbind';
|
|
26
|
+
*
|
|
27
|
+
* const ephemeral = await generateEphemeralKeyPair();
|
|
28
|
+
* if (!ephemeral.ok) {
|
|
29
|
+
* throw new Error(`Key generation failed: ${ephemeral.error}`);
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* console.log('Ephemeral public key (32 bytes):', ephemeral.value.rawPublicKey);
|
|
33
|
+
* // Use ephemeral.value.privateKey for ECDH, then discard
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export async function generateEphemeralKeyPair() {
|
|
37
|
+
try {
|
|
38
|
+
// SAFETY: X25519 generateKey always returns CryptoKeyPair
|
|
39
|
+
const keyPair = await crypto.subtle.generateKey({ name: 'X25519' }, true, ['deriveBits']);
|
|
40
|
+
const rawPub = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey));
|
|
41
|
+
return ok({
|
|
42
|
+
privateKey: keyPair.privateKey,
|
|
43
|
+
publicKey: keyPair.publicKey,
|
|
44
|
+
rawPublicKey: rawPub,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return err('KEYGEN_FAILED');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/* -- Key Import -- */
|
|
52
|
+
/**
|
|
53
|
+
* Import a raw 32-byte X25519 public key into a CryptoKey.
|
|
54
|
+
*
|
|
55
|
+
* Used by the receiver to import the sender's ephemeral public
|
|
56
|
+
* key from the envelope for ECDH derivation.
|
|
57
|
+
*
|
|
58
|
+
* @param rawPublicKey - Raw 32-byte X25519 public key.
|
|
59
|
+
* @returns Result containing the imported CryptoKey or error.
|
|
60
|
+
*/
|
|
61
|
+
export async function importX25519PublicKey(rawPublicKey) {
|
|
62
|
+
if (rawPublicKey.length !== X25519_RAW_KEY_BYTES) {
|
|
63
|
+
return err('INVALID_KEY_LENGTH:EXPECTED_32');
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const key = await crypto.subtle.importKey('raw', toArrayBuffer(rawPublicKey), { name: 'X25519' }, true, []);
|
|
67
|
+
return ok(key);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return err('IMPORT_FAILED');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/* -- ECDH Key Derivation -- */
|
|
74
|
+
/**
|
|
75
|
+
* Derive a shared AES-256-GCM key via X25519 ECDH.
|
|
76
|
+
*
|
|
77
|
+
* Performs ECDH key agreement between a local private key and a
|
|
78
|
+
* remote public key, producing 256 bits of shared secret that
|
|
79
|
+
* are imported as an AES-256-GCM key.
|
|
80
|
+
*
|
|
81
|
+
* @param localPrivateKey - Local X25519 private key.
|
|
82
|
+
* @param remotePublicKey - Remote X25519 public key (CryptoKey).
|
|
83
|
+
* @returns Result containing the derived AES-256-GCM key or error.
|
|
84
|
+
*/
|
|
85
|
+
export async function deriveSharedKeyECDH(localPrivateKey, remotePublicKey) {
|
|
86
|
+
try {
|
|
87
|
+
const sharedBits = await crypto.subtle.deriveBits({ name: 'X25519', public: remotePublicKey }, localPrivateKey, AES_KEY_BITS);
|
|
88
|
+
const aesKey = await crypto.subtle.importKey('raw', sharedBits, { name: 'AES-GCM', length: AES_KEY_BITS }, false, ['encrypt', 'decrypt']);
|
|
89
|
+
return ok(aesKey);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return err('DERIVE_FAILED');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/* -- High-Level API -- */
|
|
96
|
+
/**
|
|
97
|
+
* Perform sender-side ECDH key agreement.
|
|
98
|
+
*
|
|
99
|
+
* Generates an ephemeral X25519 key pair, derives a shared key
|
|
100
|
+
* with the recipient's static X25519 public key, and returns
|
|
101
|
+
* both the shared key and the ephemeral public key to include
|
|
102
|
+
* in the envelope.
|
|
103
|
+
*
|
|
104
|
+
* @param recipientX25519PubKey - Recipient's static X25519 public key.
|
|
105
|
+
* @returns Result containing the key agreement result or error.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* import { senderKeyAgreement, importX25519PublicKey } from '@private.me/xbind';
|
|
110
|
+
*
|
|
111
|
+
* // Import recipient's X25519 public key
|
|
112
|
+
* const recipientPubKeyRaw = new Uint8Array(32); // From recipient's identity
|
|
113
|
+
* const recipientPubKey = await importX25519PublicKey(recipientPubKeyRaw);
|
|
114
|
+
* if (!recipientPubKey.ok) throw new Error(recipientPubKey.error);
|
|
115
|
+
*
|
|
116
|
+
* // Perform key agreement
|
|
117
|
+
* const agreement = await senderKeyAgreement(recipientPubKey.value);
|
|
118
|
+
* if (!agreement.ok) throw new Error(agreement.error);
|
|
119
|
+
*
|
|
120
|
+
* // Use shared key for encryption
|
|
121
|
+
* const { sharedKey, ephemeralPublicKey } = agreement.value;
|
|
122
|
+
* // Include ephemeralPublicKey in envelope header
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export async function senderKeyAgreement(recipientX25519PubKey) {
|
|
126
|
+
const ephemeral = await generateEphemeralKeyPair();
|
|
127
|
+
if (!ephemeral.ok)
|
|
128
|
+
return ephemeral;
|
|
129
|
+
const shared = await deriveSharedKeyECDH(ephemeral.value.privateKey, recipientX25519PubKey);
|
|
130
|
+
if (!shared.ok)
|
|
131
|
+
return shared;
|
|
132
|
+
return ok({
|
|
133
|
+
sharedKey: shared.value,
|
|
134
|
+
ephemeralPublicKey: ephemeral.value.rawPublicKey,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Perform receiver-side ECDH key agreement.
|
|
139
|
+
*
|
|
140
|
+
* Imports the sender's ephemeral public key from the envelope
|
|
141
|
+
* and derives the shared key using the receiver's static X25519
|
|
142
|
+
* private key.
|
|
143
|
+
*
|
|
144
|
+
* @param receiverPrivateKey - Receiver's static X25519 private key.
|
|
145
|
+
* @param senderEphemeralPubRaw - Sender's ephemeral public key (raw bytes).
|
|
146
|
+
* @returns Result containing the derived AES-256-GCM key or error.
|
|
147
|
+
*/
|
|
148
|
+
export async function receiverKeyAgreement(receiverPrivateKey, senderEphemeralPubRaw) {
|
|
149
|
+
const imported = await importX25519PublicKey(senderEphemeralPubRaw);
|
|
150
|
+
if (!imported.ok)
|
|
151
|
+
return imported;
|
|
152
|
+
return deriveSharedKeyECDH(receiverPrivateKey, imported.value);
|
|
153
|
+
}
|
|
154
|
+
/* -- Hybrid Key Agreement (X25519 + ML-KEM-768) -- */
|
|
155
|
+
const HYBRID_HKDF_INFO = 'xail-hybrid-kem-v2';
|
|
156
|
+
/**
|
|
157
|
+
* Combine X25519 + ML-KEM shared secrets using IETF X-Wing combiner.
|
|
158
|
+
*
|
|
159
|
+
* IETF draft-connolly-cfrg-xwing-kem-10 compliant combiner that includes:
|
|
160
|
+
* - ML-KEM shared secret (32B)
|
|
161
|
+
* - ML-KEM ciphertext (1088B for ML-KEM-768)
|
|
162
|
+
* - X25519 shared secret (32B)
|
|
163
|
+
* - X25519 ephemeral public key (32B)
|
|
164
|
+
* - X25519 recipient public key (32B)
|
|
165
|
+
* - ML-KEM recipient public key (1184B for ML-KEM-768)
|
|
166
|
+
*
|
|
167
|
+
* This prevents Unknown Key Share (UKS) attacks, transcript manipulation,
|
|
168
|
+
* and session confusion by binding the key material to the full protocol transcript.
|
|
169
|
+
*
|
|
170
|
+
* Session 159 - CRYPTO-1: Upgraded from naive concatenation to IETF standard.
|
|
171
|
+
*
|
|
172
|
+
* @param mlKemShared - 32-byte ML-KEM-768 shared secret
|
|
173
|
+
* @param mlKemCiphertext - 1088-byte ML-KEM-768 ciphertext
|
|
174
|
+
* @param x25519Shared - 32-byte raw X25519 ECDH shared secret
|
|
175
|
+
* @param x25519EphemeralPub - 32-byte sender's ephemeral X25519 public key
|
|
176
|
+
* @param x25519RecipientPub - 32-byte recipient's static X25519 public key
|
|
177
|
+
* @param mlKemRecipientPub - 1184-byte recipient's ML-KEM-768 public key
|
|
178
|
+
* @returns AES-256-GCM CryptoKey derived using IETF X-Wing combiner
|
|
179
|
+
*/
|
|
180
|
+
export async function combineSharedSecrets(mlKemShared, mlKemCiphertext, x25519Shared, x25519EphemeralPub, x25519RecipientPub, mlKemRecipientPub) {
|
|
181
|
+
try {
|
|
182
|
+
// IETF X-Wing combiner: concat all key material + ciphertexts + public keys
|
|
183
|
+
// This binds the shared secret to the full protocol transcript
|
|
184
|
+
const totalLength = mlKemShared.length + // 32B
|
|
185
|
+
mlKemCiphertext.length + // 1088B
|
|
186
|
+
x25519Shared.length + // 32B
|
|
187
|
+
x25519EphemeralPub.length + // 32B
|
|
188
|
+
x25519RecipientPub.length + // 32B
|
|
189
|
+
mlKemRecipientPub.length; // 1184B
|
|
190
|
+
// Total: 2400B
|
|
191
|
+
const combined = new Uint8Array(totalLength);
|
|
192
|
+
let offset = 0;
|
|
193
|
+
// Concatenate in IETF X-Wing order
|
|
194
|
+
combined.set(mlKemShared, offset);
|
|
195
|
+
offset += mlKemShared.length;
|
|
196
|
+
combined.set(mlKemCiphertext, offset);
|
|
197
|
+
offset += mlKemCiphertext.length;
|
|
198
|
+
combined.set(x25519Shared, offset);
|
|
199
|
+
offset += x25519Shared.length;
|
|
200
|
+
combined.set(x25519EphemeralPub, offset);
|
|
201
|
+
offset += x25519EphemeralPub.length;
|
|
202
|
+
combined.set(x25519RecipientPub, offset);
|
|
203
|
+
offset += x25519RecipientPub.length;
|
|
204
|
+
combined.set(mlKemRecipientPub, offset);
|
|
205
|
+
// HKDF-Extract with domain separation
|
|
206
|
+
const domain = new TextEncoder().encode('XWingKEM');
|
|
207
|
+
const baseKey = await crypto.subtle.importKey('raw', toArrayBuffer(combined), 'HKDF', false, ['deriveBits']);
|
|
208
|
+
const derived = await crypto.subtle.deriveBits({
|
|
209
|
+
name: 'HKDF',
|
|
210
|
+
hash: 'SHA-256',
|
|
211
|
+
salt: domain, // Domain separation (not zero salt)
|
|
212
|
+
info: new TextEncoder().encode(HYBRID_HKDF_INFO),
|
|
213
|
+
}, baseKey, AES_KEY_BITS);
|
|
214
|
+
const aesKey = await crypto.subtle.importKey('raw', derived, { name: 'AES-GCM', length: AES_KEY_BITS }, false, ['encrypt', 'decrypt']);
|
|
215
|
+
return ok(aesKey);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return err('HKDF_FAILED');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Perform sender-side hybrid key agreement (X25519 + ML-KEM-768).
|
|
223
|
+
*
|
|
224
|
+
* 1. Generate ephemeral X25519, ECDH → x25519SharedBits
|
|
225
|
+
* 2. ML-KEM-768 encapsulate → kemCiphertext + mlKemShared
|
|
226
|
+
* 3. HKDF(x25519Shared || mlKemShared) → AES-256-GCM key
|
|
227
|
+
*
|
|
228
|
+
* @param recipientX25519Pub - Recipient's static X25519 public key.
|
|
229
|
+
* @param recipientMlKemPub - Recipient's ML-KEM-768 public key (1184B).
|
|
230
|
+
* @returns Hybrid key agreement result with shared key, ephemeral pub, and KEM ciphertext.
|
|
231
|
+
*/
|
|
232
|
+
export async function senderHybridKeyAgreement(recipientX25519Pub, recipientMlKemPub) {
|
|
233
|
+
// Step 1: Ephemeral X25519 ECDH
|
|
234
|
+
const ephemeral = await generateEphemeralKeyPair();
|
|
235
|
+
if (!ephemeral.ok)
|
|
236
|
+
return ephemeral;
|
|
237
|
+
let x25519Shared;
|
|
238
|
+
try {
|
|
239
|
+
x25519Shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'X25519', public: recipientX25519Pub }, ephemeral.value.privateKey, AES_KEY_BITS));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return err('DERIVE_FAILED');
|
|
243
|
+
}
|
|
244
|
+
// Step 2: ML-KEM-768 encapsulate
|
|
245
|
+
let kemCipherText;
|
|
246
|
+
let kemSharedSecret;
|
|
247
|
+
try {
|
|
248
|
+
const mlkem = await createMlKem768();
|
|
249
|
+
const [cipherText, sharedSecret] = mlkem.encap(recipientMlKemPub);
|
|
250
|
+
kemCipherText = cipherText;
|
|
251
|
+
kemSharedSecret = sharedSecret;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return err('KEM_ENCAPSULATE_FAILED');
|
|
255
|
+
}
|
|
256
|
+
// Step 3: Export recipient's X25519 public key to raw bytes
|
|
257
|
+
let recipientX25519RawPub;
|
|
258
|
+
try {
|
|
259
|
+
const exported = await crypto.subtle.exportKey('raw', recipientX25519Pub);
|
|
260
|
+
recipientX25519RawPub = new Uint8Array(exported);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return err('EXPORT_KEY_FAILED');
|
|
264
|
+
}
|
|
265
|
+
// Step 4: Combine via IETF X-Wing combiner
|
|
266
|
+
const combined = await combineSharedSecrets(kemSharedSecret, // ML-KEM shared secret
|
|
267
|
+
kemCipherText, // ML-KEM ciphertext
|
|
268
|
+
x25519Shared, // X25519 shared secret
|
|
269
|
+
ephemeral.value.rawPublicKey, // X25519 ephemeral public key
|
|
270
|
+
recipientX25519RawPub, // X25519 recipient public key
|
|
271
|
+
recipientMlKemPub);
|
|
272
|
+
if (!combined.ok)
|
|
273
|
+
return combined;
|
|
274
|
+
return ok({
|
|
275
|
+
sharedKey: combined.value,
|
|
276
|
+
ephemeralPublicKey: ephemeral.value.rawPublicKey,
|
|
277
|
+
kemCiphertext: kemCipherText,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Perform receiver-side hybrid key agreement (X25519 + ML-KEM-768).
|
|
282
|
+
*
|
|
283
|
+
* 1. Import ephemeral X25519, ECDH → x25519SharedBits
|
|
284
|
+
* 2. ML-KEM-768 decapsulate → mlKemShared
|
|
285
|
+
* 3. IETF X-Wing combiner → same AES-256-GCM key
|
|
286
|
+
*
|
|
287
|
+
* Session 159 - CRYPTO-1: Updated to use IETF X-Wing combiner with public keys.
|
|
288
|
+
*
|
|
289
|
+
* @param x25519PrivateKey - Receiver's static X25519 private key.
|
|
290
|
+
* @param x25519PublicKeyRaw - Receiver's static X25519 public key (32B raw bytes).
|
|
291
|
+
* @param ephPubRaw - Sender's ephemeral X25519 public key (32B).
|
|
292
|
+
* @param kemCiphertext - ML-KEM-768 ciphertext from sender (1088B).
|
|
293
|
+
* @param mlKemSecretKey - Receiver's ML-KEM-768 secret key (2400B).
|
|
294
|
+
* @param mlKemPublicKey - Receiver's ML-KEM-768 public key (1184B).
|
|
295
|
+
* @returns AES-256-GCM shared key or error.
|
|
296
|
+
*/
|
|
297
|
+
export async function receiverHybridKeyAgreement(x25519PrivateKey, x25519PublicKeyRaw, ephPubRaw, kemCiphertext, mlKemSecretKey, mlKemPublicKey) {
|
|
298
|
+
// Step 1: X25519 ECDH
|
|
299
|
+
const imported = await importX25519PublicKey(ephPubRaw);
|
|
300
|
+
if (!imported.ok)
|
|
301
|
+
return imported;
|
|
302
|
+
let x25519Shared;
|
|
303
|
+
try {
|
|
304
|
+
x25519Shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'X25519', public: imported.value }, x25519PrivateKey, AES_KEY_BITS));
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return err('DERIVE_FAILED');
|
|
308
|
+
}
|
|
309
|
+
// Step 2: ML-KEM-768 decapsulate
|
|
310
|
+
let mlKemShared;
|
|
311
|
+
try {
|
|
312
|
+
const mlkem = await createMlKem768();
|
|
313
|
+
mlKemShared = mlkem.decap(kemCiphertext, mlKemSecretKey);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return err('KEM_DECAPSULATE_FAILED');
|
|
317
|
+
}
|
|
318
|
+
// Step 3: Combine via IETF X-Wing combiner
|
|
319
|
+
return combineSharedSecrets(mlKemShared, // ML-KEM shared secret
|
|
320
|
+
kemCiphertext, // ML-KEM ciphertext
|
|
321
|
+
x25519Shared, // X25519 shared secret
|
|
322
|
+
ephPubRaw, // X25519 ephemeral public key (from sender)
|
|
323
|
+
x25519PublicKeyRaw, // X25519 recipient public key (own)
|
|
324
|
+
mlKemPublicKey);
|
|
325
|
+
}
|