@qpjoy/mx-launcher-core 0.2.0

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 ADDED
@@ -0,0 +1,145 @@
1
+ # @qpjoy/mx-launcher-core
2
+
3
+ `launcher-core` is the shared protocol package for all MX Launcher SDKs. It must
4
+ not own Electron runtime state, WireGuard, DNS, downloads, or UI. Its job is to
5
+ make standalone and embed packages speak the same language.
6
+
7
+ ## Ownership
8
+
9
+ Put shared contracts here when they are used by both standalone brokers and
10
+ embed clients:
11
+
12
+ - manifest schemas
13
+ - capability names
14
+ - event names
15
+ - request and response envelopes
16
+ - broker handshake payloads
17
+ - channel registry record shape
18
+ - signed cache metadata
19
+ - release artifact metadata
20
+ - rollout policy input and output
21
+ - protocol and ABI version checks
22
+ - shared validation helpers
23
+
24
+ Do not put role-specific side effects here. Core code should be deterministic
25
+ and testable without Electron, network adapters, or privileged host access.
26
+
27
+ ## Compatibility
28
+
29
+ Same-major packages must be protocol-compatible. Core is the place that defines
30
+ what compatibility means:
31
+
32
+ ```ts
33
+ export const launcherProtocolMajor = 2;
34
+ export const brokerAbiMajor = 2;
35
+ ```
36
+
37
+ Future implementation should expose helpers similar to:
38
+
39
+ ```ts
40
+ assertCompatibleBroker({
41
+ embedProtocolMajor,
42
+ brokerProtocolMajor,
43
+ minBrokerAbi,
44
+ brokerAbi
45
+ });
46
+ ```
47
+
48
+ Rules:
49
+
50
+ - patch and minor versions can add optional fields.
51
+ - required field changes, removed events, renamed capabilities, or incompatible
52
+ handshake changes require a new major.
53
+ - unknown optional fields must be ignored by older compatible peers.
54
+ - incompatible peers must fail closed with a typed reason, not a generic network
55
+ error.
56
+
57
+ ## Manifest Model
58
+
59
+ All products should use one manifest shape with role-specific sections:
60
+
61
+ ```ts
62
+ type LauncherManifest = {
63
+ appId: string;
64
+ productId: string;
65
+ launcherMode: 'standalone' | 'embed';
66
+ sdkAbiVersion: string;
67
+ requiredCapabilities: string[];
68
+ network: {
69
+ scope: 'owner' | 'broker-session';
70
+ serviceVip?: string | null;
71
+ };
72
+ standalone?: {
73
+ ownsNetwork: true;
74
+ brokerEnabled: true;
75
+ };
76
+ embed?: {
77
+ standaloneChannelProductId: string;
78
+ launchWithoutBroker: 'blocked' | 'prompt-open-standalone';
79
+ };
80
+ };
81
+ ```
82
+
83
+ The same shape lets K8s admin, AppCenter, standalone products, and embed apps
84
+ reason about bindings without bespoke per-package schemas.
85
+
86
+ `createLauncherEmbedManifest()` produces the default embed variant for this
87
+ shape. It sets `runtimeContractVersion: 0.1`, `launcherMode: embed`,
88
+ `network.scope: broker-session`, and the configured
89
+ `embed.standaloneChannelProductId`.
90
+
91
+ `network.scope` is the important distinction for the embed IP question:
92
+ standalone products are `owner` and may allocate peer leases; embed apps are
93
+ `broker-session` and inherit network/user/permission/update state from their
94
+ selected standalone channel.
95
+
96
+ ## Channel Registry Record
97
+
98
+ The registry stores running standalone brokers only:
99
+
100
+ ```ts
101
+ type LauncherChannelRecord = {
102
+ productId: string;
103
+ instanceId: string;
104
+ pid: number;
105
+ socketPath: string;
106
+ brokerAbiVersion: string;
107
+ protocolVersion: string;
108
+ capabilities: string[];
109
+ heartbeatAt: string;
110
+ };
111
+ ```
112
+
113
+ The registry is short-lived discovery data. It must not contain WireGuard keys,
114
+ DNS state, long-lived user tokens, or release secrets.
115
+
116
+ ## Capability Model
117
+
118
+ Capabilities are strings owned by core. Examples:
119
+
120
+ ```txt
121
+ catalog.read
122
+ app.install
123
+ app.launch
124
+ app.update.manage
125
+ user.session
126
+ network.proxy
127
+ network.status
128
+ release.resolve
129
+ audit.record
130
+ ```
131
+
132
+ Standalone SDKs enforce capabilities. Embed SDKs only request them.
133
+
134
+ ## Test Expectations
135
+
136
+ Core should own contract fixtures. Standalone and embed packages should consume
137
+ the same fixtures in their tests:
138
+
139
+ - handshake success
140
+ - broker ABI mismatch
141
+ - missing required capability
142
+ - stale binding cache
143
+ - release policy match
144
+ - rollout bucket stability
145
+ - signed artifact verification failure
@@ -0,0 +1,520 @@
1
+ export type LauncherProductMode = 'standalone' | 'embed';
2
+ export type LauncherIdentityKind = 'user' | 'anonymous';
3
+ export type LauncherProductUpdatePolicy = 'launcher-managed' | 'app-managed' | 'host-managed';
4
+ export type LauncherNetworkScope = 'owner' | 'broker-session';
5
+ export declare const launcherProtocolVersion = "2";
6
+ export declare const launcherProtocolMajor = 2;
7
+ export declare const brokerAbiVersion = "2";
8
+ export declare const brokerAbiMajor = 2;
9
+ export declare const launcherEmbedRuntimeContractVersion = "0.1";
10
+ export declare const launcherCapabilities: readonly ["catalog.read", "app.install", "app.launch", "app.update.manage", "user.session", "network.proxy", "network.status", "release.resolve", "audit.record"];
11
+ export type LauncherCapability = typeof launcherCapabilities[number] | (string & {});
12
+ export declare const launcherEmbedRuntimeEvents: readonly ["embed.init", "embed.ready", "embed.error", "embed.logs", "auth.changed", "network.changed", "permission.changed", "app.open", "app.close"];
13
+ export type LauncherEmbedRuntimeEventName = typeof launcherEmbedRuntimeEvents[number] | (string & {});
14
+ export declare const launcherEmbedBrokerRequests: readonly ["user.session", "network.status", "network.proxy", "network.dns.policy", "network.pac.policy", "app.open", "app.close", "app.logs", "app.update.check"];
15
+ export type LauncherEmbedBrokerRequestName = typeof launcherEmbedBrokerRequests[number] | (string & {});
16
+ export interface LauncherClientOptions {
17
+ baseUrl: string;
18
+ fetchImpl?: FetchLike;
19
+ }
20
+ export type FetchLike = (input: string, init?: FetchInitLike) => Promise<ResponseLike>;
21
+ export interface FetchInitLike {
22
+ method?: string;
23
+ headers?: Record<string, string>;
24
+ body?: string;
25
+ }
26
+ export interface ResponseLike {
27
+ ok: boolean;
28
+ status: number;
29
+ statusText?: string;
30
+ text(): Promise<string>;
31
+ }
32
+ export interface LauncherWireGuardKeyPair {
33
+ privateKey: string;
34
+ publicKey: string;
35
+ }
36
+ export interface LauncherWireGuardKeyMaterial {
37
+ privateKey: string | null;
38
+ publicKey: string;
39
+ source: 'generated' | 'provider' | 'provided-private-key' | 'provided-public-key';
40
+ }
41
+ export type LauncherWireGuardKeyProvider = () => LauncherWireGuardKeyPair | Promise<LauncherWireGuardKeyPair>;
42
+ export interface LauncherManifest {
43
+ appId: string;
44
+ productId: string;
45
+ displayName?: string;
46
+ description?: string;
47
+ packageName?: string;
48
+ category?: string;
49
+ launcherMode: LauncherProductMode;
50
+ sdkVersion?: string;
51
+ appVersion?: string;
52
+ sdkAbiVersion?: string;
53
+ protocolVersion?: string;
54
+ runtimeContractVersion?: string;
55
+ requiredCapabilities: LauncherCapability[];
56
+ network?: {
57
+ scope?: LauncherNetworkScope;
58
+ serviceVip?: string | null;
59
+ };
60
+ standalone?: {
61
+ ownsNetwork: true;
62
+ brokerEnabled: boolean;
63
+ };
64
+ embed?: {
65
+ standaloneChannelProductId: string;
66
+ launchWithoutBroker: 'blocked' | 'prompt-open-standalone';
67
+ };
68
+ }
69
+ export interface LauncherEmbedManifestInput {
70
+ appId: string;
71
+ productId?: string | null;
72
+ displayName?: string | null;
73
+ description?: string | null;
74
+ packageName?: string | null;
75
+ category?: string | null;
76
+ sdkVersion?: string | null;
77
+ appVersion?: string | null;
78
+ sdkAbiVersion?: string | null;
79
+ protocolVersion?: string | null;
80
+ runtimeContractVersion?: string | null;
81
+ standaloneChannelProductId?: string | null;
82
+ launchWithoutBroker?: 'blocked' | 'prompt-open-standalone' | null;
83
+ requiredCapabilities?: LauncherCapability[] | null;
84
+ serviceVip?: string | null;
85
+ }
86
+ export interface LauncherChannelRecord {
87
+ productId: string;
88
+ instanceId: string;
89
+ pid: number;
90
+ socketPath: string;
91
+ brokerAbiVersion: string;
92
+ protocolVersion: string;
93
+ capabilities: LauncherCapability[];
94
+ heartbeatAt: string;
95
+ displayName?: string;
96
+ }
97
+ export type LauncherEmbedConnectionState = 'idle' | 'discovering-broker' | 'standalone-required' | 'broker-incompatible' | 'capability-denied' | 'connected' | 'network-ready' | 'blocked' | 'disconnected';
98
+ export interface LauncherBrokerHandshakeRequest {
99
+ appId: string;
100
+ productId: string;
101
+ launcherMode: 'embed';
102
+ sdkVersion?: string;
103
+ appVersion?: string;
104
+ protocolVersion: string;
105
+ minBrokerAbiVersion: string;
106
+ standaloneChannelProductId: string;
107
+ requestedCapabilities: LauncherCapability[];
108
+ installId?: string | null;
109
+ deviceId?: string | null;
110
+ userId?: string | null;
111
+ requestId?: string | null;
112
+ }
113
+ export type LauncherBrokerHandshakeDeniedReason = 'standalone-required' | 'broker-incompatible' | 'capability-denied' | 'policy-blocked';
114
+ export interface LauncherBrokerCapabilitySession {
115
+ sessionId: string;
116
+ appId: string;
117
+ productId: string;
118
+ launcherMode: 'embed';
119
+ networkScope: 'broker-session';
120
+ standaloneChannelProductId: string;
121
+ channel: LauncherChannelRecord;
122
+ grantedCapabilities: LauncherCapability[];
123
+ deniedCapabilities: LauncherCapability[];
124
+ userId: string | null;
125
+ installId: string | null;
126
+ deviceId: string | null;
127
+ issuedAt: string;
128
+ expiresAt: string | null;
129
+ }
130
+ export type LauncherBrokerHandshakeResult = {
131
+ ok: true;
132
+ state: 'connected' | 'network-ready';
133
+ session: LauncherBrokerCapabilitySession;
134
+ } | {
135
+ ok: false;
136
+ state: LauncherBrokerHandshakeDeniedReason;
137
+ reason: LauncherBrokerHandshakeDeniedReason;
138
+ message: string;
139
+ channel?: LauncherChannelRecord | null;
140
+ missingCapabilities?: LauncherCapability[];
141
+ };
142
+ export interface LauncherBrokerCompatibilityInput {
143
+ embedProtocolVersion?: string | number | null;
144
+ brokerProtocolVersion?: string | number | null;
145
+ minBrokerAbiVersion?: string | number | null;
146
+ brokerAbiVersion?: string | number | null;
147
+ }
148
+ export interface LauncherBrokerCompatibilityResult {
149
+ compatible: boolean;
150
+ reason: 'compatible' | 'protocol-major-mismatch' | 'broker-abi-too-old' | 'invalid-version';
151
+ message: string;
152
+ }
153
+ export interface AnonymousEnrollmentRequest {
154
+ productId?: string;
155
+ siteId?: string;
156
+ installId?: string;
157
+ deviceId?: string;
158
+ deviceLabel?: string;
159
+ platform?: string;
160
+ publicKey?: string;
161
+ relayMode?: string;
162
+ requestId?: string;
163
+ }
164
+ export interface AnonymousEnrollment {
165
+ anonymousPrincipalId: string;
166
+ installId: string;
167
+ deviceId: string;
168
+ productId: string;
169
+ siteId: string;
170
+ environment: string;
171
+ overlayIp: string;
172
+ relayMode: string;
173
+ publicKey: string | null;
174
+ createdAt: string;
175
+ userId: string | null;
176
+ }
177
+ export interface LauncherNetworkSnapshotInput {
178
+ installId?: string;
179
+ deviceId?: string;
180
+ siteId?: string | null;
181
+ userId?: string | null;
182
+ publicKey?: string | null;
183
+ appId?: string;
184
+ launcherMode?: LauncherProductMode | null;
185
+ requestId?: string;
186
+ }
187
+ export interface LauncherNetworkLeaseInput {
188
+ appId?: string | null;
189
+ productId?: string | null;
190
+ mode?: LauncherProductMode | string | null;
191
+ identityKind?: LauncherIdentityKind | string | null;
192
+ installId?: string | null;
193
+ deviceId?: string | null;
194
+ siteId?: string | null;
195
+ userId?: string | null;
196
+ publicKey?: string | null;
197
+ deviceLabel?: string | null;
198
+ platform?: string | null;
199
+ requestedBy?: string | null;
200
+ requestId?: string | null;
201
+ sdkTestMode?: boolean | string | null;
202
+ }
203
+ export interface LauncherNetworkSessionInput extends LauncherNetworkLeaseInput {
204
+ appId?: string | null;
205
+ launcherMode?: LauncherProductMode | string | null;
206
+ privateKey?: string | null;
207
+ keyPair?: LauncherWireGuardKeyPair | null;
208
+ keyProvider?: LauncherWireGuardKeyProvider | null;
209
+ snapshotRequestId?: string | null;
210
+ }
211
+ export interface LauncherProductNetworkInput {
212
+ productId?: string | null;
213
+ displayName?: string | null;
214
+ mode?: LauncherProductMode | string | null;
215
+ networkScope?: LauncherNetworkScope | string | null;
216
+ productIndex?: number | null;
217
+ internalControlIp?: string | null;
218
+ domesticGatewayIp?: string | null;
219
+ dnsServer?: string | null;
220
+ serviceVip?: string | null;
221
+ userCidr?: string | null;
222
+ anonymousCidr?: string | null;
223
+ userLeaseStart?: string | null;
224
+ userLeaseEnd?: string | null;
225
+ anonymousLeaseStart?: string | null;
226
+ anonymousLeaseEnd?: string | null;
227
+ defaultDomesticSiteId?: string | null;
228
+ defaultOverseaSiteId?: string | null;
229
+ updatePolicy?: LauncherProductUpdatePolicy | string | null;
230
+ rateLimitProfile?: string | null;
231
+ dnsPolicyId?: string | null;
232
+ licensePolicyId?: string | null;
233
+ enabled?: boolean | null;
234
+ requestedBy?: string | null;
235
+ requestId?: string | null;
236
+ }
237
+ export interface LauncherProductNetwork {
238
+ productId: string;
239
+ displayName: string;
240
+ mode: LauncherProductMode;
241
+ networkScope: LauncherNetworkScope;
242
+ productIndex: number;
243
+ fabricCidr: '10.88.0.0/16';
244
+ internalControlIp: string;
245
+ domesticGatewayIp: string;
246
+ dnsServer: string;
247
+ serviceVip: string;
248
+ userCidr: string;
249
+ anonymousCidr: string;
250
+ userLeaseStart: string;
251
+ userLeaseEnd: string;
252
+ anonymousLeaseStart: string;
253
+ anonymousLeaseEnd: string;
254
+ defaultDomesticSiteId: string;
255
+ defaultOverseaSiteId: string;
256
+ updatePolicy: LauncherProductUpdatePolicy;
257
+ rateLimitProfile: string;
258
+ dnsPolicyId: string;
259
+ licensePolicyId: string;
260
+ enabled: boolean;
261
+ notes: string[];
262
+ createdBy: string;
263
+ createdAt: string;
264
+ updatedBy: string;
265
+ updatedAt: string;
266
+ }
267
+ export interface LauncherNetworkLease {
268
+ leaseId: string;
269
+ leaseKey: string;
270
+ environment: string;
271
+ productId: string;
272
+ launcherMode: LauncherProductMode;
273
+ identityKind: LauncherIdentityKind;
274
+ sequence: number;
275
+ installId: string;
276
+ deviceId: string;
277
+ siteId: string;
278
+ userId: string | null;
279
+ cidr: string;
280
+ leaseIp: string;
281
+ serviceVip: string;
282
+ internalControlIp: string;
283
+ domesticGatewayIp: string;
284
+ domesticSiteId: string;
285
+ overseaSiteId: string;
286
+ publicKey: string | null;
287
+ deviceLabel: string | null;
288
+ platform: string | null;
289
+ status: 'active';
290
+ createdBy: string;
291
+ createdAt: string;
292
+ updatedBy: string;
293
+ updatedAt: string;
294
+ }
295
+ export interface LauncherNetworkSnapshot {
296
+ snapshotId: string;
297
+ environment: string;
298
+ appId: string;
299
+ installId: string;
300
+ deviceId: string;
301
+ userId: string | null;
302
+ mode: 'guest' | 'user';
303
+ overlayPolicy: {
304
+ productId: string;
305
+ launcherMode: LauncherProductMode;
306
+ identityKind: LauncherIdentityKind;
307
+ cidr: string;
308
+ leaseIp: string;
309
+ relayMode: 'h2i';
310
+ };
311
+ topology: LauncherNetworkTopology;
312
+ capabilities: {
313
+ wireGuard: boolean;
314
+ splitDns: boolean;
315
+ pac: boolean;
316
+ tun: boolean;
317
+ systemProxy: boolean;
318
+ };
319
+ dns: {
320
+ authority: 'internal-coredns';
321
+ matchDomains: string[];
322
+ fallback: 'system';
323
+ };
324
+ pac: {
325
+ priority: string[];
326
+ };
327
+ signatures: {
328
+ algorithm: string;
329
+ digest: string;
330
+ issuer: string;
331
+ };
332
+ issuedAt: string;
333
+ }
334
+ export interface LauncherNetworkTopology {
335
+ model: 'internal-authority-domestic-relay-oversea-access-v1';
336
+ product: {
337
+ productId: string;
338
+ displayName: string;
339
+ mode: LauncherProductMode;
340
+ serviceVip: string;
341
+ internalControlIp: string;
342
+ domesticGatewayIp: string;
343
+ dnsServer: string;
344
+ userCidr: string;
345
+ anonymousCidr: string;
346
+ updatePolicy: LauncherProductUpdatePolicy;
347
+ rateLimitProfile: string;
348
+ dnsPolicyId: string;
349
+ licensePolicyId: string;
350
+ };
351
+ homeLease: {
352
+ mode: 'guest' | 'user';
353
+ ip: string;
354
+ cidr: string;
355
+ };
356
+ domestic: {
357
+ siteId: string;
358
+ role: 'relay-proxy-cache-forwarder';
359
+ publicIpRequired: true;
360
+ gatewayIp: string;
361
+ overlayCidrs: string[];
362
+ configSource: 'internal-signed-snapshot';
363
+ storesAuthority: false;
364
+ };
365
+ internal: {
366
+ siteId: string;
367
+ publicIngress: false;
368
+ baseUrl: string;
369
+ enrollUrl: string;
370
+ configSnapshotUrl: string;
371
+ mihomoSubscriptionBaseUrl: string;
372
+ requiresEnrollLease: true;
373
+ relayPeer: {
374
+ required: true;
375
+ fixedIp: string;
376
+ initiatedBy: 'internal-outbound-to-domestic-public-wg';
377
+ purpose: string;
378
+ };
379
+ };
380
+ oversea: {
381
+ siteId: string;
382
+ role: 'hysteria2-access-site';
383
+ subscriptionAuthority: 'internal-mihomo';
384
+ trafficPath: 'direct-after-subscription';
385
+ };
386
+ subscriptions: {
387
+ mihomo: {
388
+ authority: 'internal-config-center';
389
+ siteId: string;
390
+ baseUrl: string;
391
+ fetchPath: string;
392
+ reachableVia: string[];
393
+ fallback: 'domestic-snapshot-cache';
394
+ };
395
+ };
396
+ relayPlan: {
397
+ authority: 'internal-config-center';
398
+ domesticRelay: {
399
+ siteId: string;
400
+ interfaceName: 'mx-domestic';
401
+ listenPort: 51280;
402
+ gatewayIp: string;
403
+ publicEndpoint: string | null;
404
+ publicKey: string | null;
405
+ configArtifact: string;
406
+ envArtifact: string;
407
+ };
408
+ refreshHint: {
409
+ source: 'internal-domestic-wg-secret';
410
+ mode: 'snapshot-digest';
411
+ publicEndpoint: string | null;
412
+ domesticRelayPublicKeyFingerprint: string | null;
413
+ internalServicePublicKeyFingerprint: string | null;
414
+ materialDigest: string | null;
415
+ secretUpdatedAt: string | null;
416
+ };
417
+ internalServicePeer: {
418
+ role: 'internal-service';
419
+ fixedIp: string;
420
+ allowedIps: string[];
421
+ configArtifact: string;
422
+ privateKeyPlacement: 'internal-only';
423
+ direction: 'internal-outbound-to-domestic-public-wg';
424
+ };
425
+ internalDirectPeer?: {
426
+ role: 'internal-direct-service';
427
+ enabled: boolean;
428
+ fixedIp: string;
429
+ endpoint: string | null;
430
+ listenPort: number;
431
+ publicKey: string | null;
432
+ allowedIps: string[];
433
+ configArtifact: string;
434
+ peerMutation: 'append-home-peer-after-enroll';
435
+ fallback: 'domestic-wg-relay';
436
+ };
437
+ homePeer: {
438
+ role: 'guest' | 'user';
439
+ leaseIp: string;
440
+ cidr: string;
441
+ allowedIps: string[];
442
+ publicKey: string | null;
443
+ publicKeyStatus: 'ready-to-append' | 'pending-public-key';
444
+ provisionedBy: 'internal-signed-relay-lease';
445
+ domesticMutation: 'append-peer-after-enroll';
446
+ };
447
+ routes: {
448
+ internalCidrs: string[];
449
+ dnsServer: string;
450
+ subscriptionReachability: 'domestic-wg-relay+h2i-proxy';
451
+ externalTraffic: 'direct-to-oversea-hysteria2-after-subscription';
452
+ };
453
+ };
454
+ gates: Record<string, boolean>;
455
+ }
456
+ export interface LauncherRoutePlan {
457
+ productId: string;
458
+ launcherMode: LauncherProductMode;
459
+ identityKind: LauncherIdentityKind;
460
+ leaseIp: string;
461
+ leaseCidr: string;
462
+ serviceVip: string;
463
+ internalControlIp: string;
464
+ internalBaseUrl: string;
465
+ domesticGatewayIp: string;
466
+ domesticRelayEndpoint: string | null;
467
+ domesticRelayPublicKey: string | null;
468
+ preferredPath: 'h2i-direct' | 'hdi-relay' | 'h2i-hybrid';
469
+ h2iDirectEnabled: boolean;
470
+ h2iDirectEndpoint: string | null;
471
+ h2iDirectPublicKey: string | null;
472
+ h2iDirectAllowedIps: string[];
473
+ domesticSiteId: string;
474
+ overseaSiteId: string;
475
+ dnsServer: string;
476
+ routeCidrs: string[];
477
+ allowedIps: string[];
478
+ updatePolicy: LauncherProductUpdatePolicy;
479
+ rateLimitProfile: string;
480
+ dnsPolicyId: string;
481
+ licensePolicyId: string;
482
+ snapshotId: string;
483
+ snapshotDigest: string;
484
+ materialDigest: string | null;
485
+ secretUpdatedAt: string | null;
486
+ refreshKey: string;
487
+ }
488
+ export interface LauncherNetworkSession {
489
+ wireGuard: LauncherWireGuardKeyMaterial;
490
+ lease: LauncherNetworkLease;
491
+ snapshot: LauncherNetworkSnapshot;
492
+ routePlan: LauncherRoutePlan;
493
+ }
494
+ export interface LauncherClient {
495
+ listProducts(): Promise<LauncherProductNetwork[]>;
496
+ getProduct(productId: string): Promise<LauncherProductNetwork>;
497
+ upsertProduct(productId: string, input: LauncherProductNetworkInput): Promise<LauncherProductNetwork>;
498
+ enrollLease(input: LauncherNetworkLeaseInput): Promise<LauncherNetworkLease>;
499
+ createSnapshot(input: LauncherNetworkSnapshotInput): Promise<LauncherNetworkSnapshot>;
500
+ enrollAnonymous(input: AnonymousEnrollmentRequest): Promise<AnonymousEnrollment>;
501
+ getConfigSnapshot(installId: string): Promise<unknown>;
502
+ }
503
+ export declare class LauncherApiError extends Error {
504
+ readonly status: number;
505
+ readonly payload: unknown;
506
+ constructor(message: string, status: number, payload: unknown);
507
+ }
508
+ export declare function createLauncherClient(options: LauncherClientOptions): LauncherClient;
509
+ export declare function createLauncherNetworkSession(client: LauncherClient, input?: LauncherNetworkSessionInput): Promise<LauncherNetworkSession>;
510
+ export declare function createLauncherWireGuardKeyPair(): LauncherWireGuardKeyPair;
511
+ export declare function wireGuardPublicKeyFromPrivateKey(privateKey: string): string;
512
+ export declare function routePlanFromSnapshot(snapshot: LauncherNetworkSnapshot): LauncherRoutePlan;
513
+ export declare function networkDigest(snapshot: LauncherNetworkSnapshot): string;
514
+ export declare function networkRefreshKey(value: LauncherNetworkSnapshot | LauncherRoutePlan | string): string;
515
+ export declare function shouldRefreshNetwork(previous: LauncherNetworkSnapshot | LauncherRoutePlan | string | null | undefined, next: LauncherNetworkSnapshot | LauncherRoutePlan | string | null | undefined): boolean;
516
+ export declare function launcherNetworkScopeForMode(mode: LauncherProductMode | string | null | undefined): LauncherNetworkScope;
517
+ export declare function createLauncherEmbedManifest(input: LauncherEmbedManifestInput): LauncherManifest;
518
+ export declare function launcherBrokerCompatibility(input: LauncherBrokerCompatibilityInput): LauncherBrokerCompatibilityResult;
519
+ export declare function assertCompatibleBroker(input: LauncherBrokerCompatibilityInput): void;
520
+ export declare function normalizeLauncherBaseUrl(baseUrl: string): string;
package/dist/index.js ADDED
@@ -0,0 +1,523 @@
1
+ export const launcherProtocolVersion = '2';
2
+ export const launcherProtocolMajor = 2;
3
+ export const brokerAbiVersion = '2';
4
+ export const brokerAbiMajor = 2;
5
+ export const launcherEmbedRuntimeContractVersion = '0.1';
6
+ export const launcherCapabilities = [
7
+ 'catalog.read',
8
+ 'app.install',
9
+ 'app.launch',
10
+ 'app.update.manage',
11
+ 'user.session',
12
+ 'network.proxy',
13
+ 'network.status',
14
+ 'release.resolve',
15
+ 'audit.record'
16
+ ];
17
+ export const launcherEmbedRuntimeEvents = [
18
+ 'embed.init',
19
+ 'embed.ready',
20
+ 'embed.error',
21
+ 'embed.logs',
22
+ 'auth.changed',
23
+ 'network.changed',
24
+ 'permission.changed',
25
+ 'app.open',
26
+ 'app.close'
27
+ ];
28
+ export const launcherEmbedBrokerRequests = [
29
+ 'user.session',
30
+ 'network.status',
31
+ 'network.proxy',
32
+ 'network.dns.policy',
33
+ 'network.pac.policy',
34
+ 'app.open',
35
+ 'app.close',
36
+ 'app.logs',
37
+ 'app.update.check'
38
+ ];
39
+ export class LauncherApiError extends Error {
40
+ status;
41
+ payload;
42
+ constructor(message, status, payload) {
43
+ super(message);
44
+ this.name = 'LauncherApiError';
45
+ this.status = status;
46
+ this.payload = payload;
47
+ }
48
+ }
49
+ export function createLauncherClient(options) {
50
+ const baseUrl = normalizeLauncherBaseUrl(options.baseUrl);
51
+ const fetchImpl = options.fetchImpl ?? getGlobalFetch();
52
+ return {
53
+ async listProducts() {
54
+ const payload = await requestJson(fetchImpl, joinUrl(baseUrl, '/internal/v1/launcher-network/products'), 'GET');
55
+ return payload.products;
56
+ },
57
+ async getProduct(productId) {
58
+ const payload = await requestJson(fetchImpl, joinUrl(baseUrl, `/internal/v1/launcher-network/products/${encodeURIComponent(productId)}`), 'GET');
59
+ return payload.product;
60
+ },
61
+ async upsertProduct(productId, input) {
62
+ const payload = await requestJson(fetchImpl, joinUrl(baseUrl, `/internal/v1/launcher-network/products/${encodeURIComponent(productId)}`), 'POST', { ...input, productId });
63
+ return payload.product;
64
+ },
65
+ async enrollLease(input) {
66
+ const payload = await requestJson(fetchImpl, joinUrl(baseUrl, '/internal/v1/launcher-network/enrollments'), 'POST', compactBody(input));
67
+ return payload.lease;
68
+ },
69
+ async createSnapshot(input) {
70
+ const payload = await requestJson(fetchImpl, joinUrl(baseUrl, '/internal/v1/launcher-network/snapshots'), 'POST', compactBody(input));
71
+ return payload.snapshot;
72
+ },
73
+ async enrollAnonymous(input) {
74
+ return requestJson(fetchImpl, joinUrl(baseUrl, '/internal/v1/enrollments/anonymous'), 'POST', compactBody(input));
75
+ },
76
+ async getConfigSnapshot(installId) {
77
+ const payload = await requestJson(fetchImpl, joinUrl(baseUrl, `/internal/v1/config/snapshots/${encodeURIComponent(installId)}`), 'GET');
78
+ return payload.snapshot;
79
+ }
80
+ };
81
+ }
82
+ export async function createLauncherNetworkSession(client, input = {}) {
83
+ const wireGuard = await resolveWireGuardKeyMaterial(input);
84
+ const productId = input.productId?.trim() || input.appId?.trim() || 'launcher';
85
+ const mode = launcherProductMode(input.mode ?? input.launcherMode ?? (productId === 'launcher' ? 'standalone' : 'embed'));
86
+ const lease = await client.enrollLease({
87
+ appId: input.appId ?? productId,
88
+ productId,
89
+ mode,
90
+ identityKind: input.identityKind,
91
+ installId: input.installId,
92
+ deviceId: input.deviceId,
93
+ siteId: input.siteId,
94
+ userId: input.userId,
95
+ publicKey: wireGuard.publicKey,
96
+ deviceLabel: input.deviceLabel,
97
+ platform: input.platform,
98
+ requestedBy: input.requestedBy ?? 'launcher-network-session',
99
+ requestId: input.requestId,
100
+ sdkTestMode: input.sdkTestMode
101
+ });
102
+ const snapshot = await client.createSnapshot({
103
+ installId: lease.installId,
104
+ deviceId: lease.deviceId,
105
+ siteId: lease.siteId,
106
+ userId: lease.userId,
107
+ publicKey: wireGuard.publicKey,
108
+ appId: lease.productId,
109
+ launcherMode: lease.launcherMode,
110
+ requestId: input.snapshotRequestId ?? input.requestId ?? undefined
111
+ });
112
+ return {
113
+ wireGuard,
114
+ lease,
115
+ snapshot,
116
+ routePlan: routePlanFromSnapshot(snapshot)
117
+ };
118
+ }
119
+ export function createLauncherWireGuardKeyPair() {
120
+ const privateKey = randomWireGuardPrivateKey();
121
+ return {
122
+ privateKey,
123
+ publicKey: wireGuardPublicKeyFromPrivateKey(privateKey)
124
+ };
125
+ }
126
+ export function wireGuardPublicKeyFromPrivateKey(privateKey) {
127
+ const scalar = decodeBase64Key(privateKey, 'WireGuard private key');
128
+ const clamped = clampWireGuardPrivateKey(scalar);
129
+ const publicKey = x25519(clamped, basePointBytes());
130
+ return bytesToBase64(publicKey);
131
+ }
132
+ export function routePlanFromSnapshot(snapshot) {
133
+ const topology = snapshot.topology;
134
+ const relayPlan = topology.relayPlan;
135
+ const product = topology.product;
136
+ const materialDigest = relayPlan.refreshHint.materialDigest;
137
+ const secretUpdatedAt = relayPlan.refreshHint.secretUpdatedAt;
138
+ return {
139
+ productId: snapshot.overlayPolicy.productId,
140
+ launcherMode: snapshot.overlayPolicy.launcherMode,
141
+ identityKind: snapshot.overlayPolicy.identityKind,
142
+ leaseIp: snapshot.overlayPolicy.leaseIp,
143
+ leaseCidr: snapshot.overlayPolicy.cidr,
144
+ serviceVip: product.serviceVip,
145
+ internalControlIp: product.internalControlIp || topology.internal.relayPeer.fixedIp,
146
+ internalBaseUrl: topology.internal.baseUrl,
147
+ domesticGatewayIp: product.domesticGatewayIp || topology.domestic.gatewayIp,
148
+ domesticRelayEndpoint: relayPlan.domesticRelay.publicEndpoint ?? relayPlan.refreshHint.publicEndpoint ?? null,
149
+ domesticRelayPublicKey: relayPlan.domesticRelay.publicKey ?? null,
150
+ preferredPath: relayPlan.internalDirectPeer?.enabled && relayPlan.internalDirectPeer.endpoint && relayPlan.internalDirectPeer.publicKey
151
+ ? 'h2i-hybrid'
152
+ : 'hdi-relay',
153
+ h2iDirectEnabled: relayPlan.internalDirectPeer?.enabled === true,
154
+ h2iDirectEndpoint: relayPlan.internalDirectPeer?.endpoint ?? null,
155
+ h2iDirectPublicKey: relayPlan.internalDirectPeer?.publicKey ?? null,
156
+ h2iDirectAllowedIps: [...(relayPlan.internalDirectPeer?.allowedIps ?? relayPlan.routes.internalCidrs)],
157
+ domesticSiteId: topology.domestic.siteId,
158
+ overseaSiteId: topology.oversea.siteId,
159
+ dnsServer: product.dnsServer || relayPlan.routes.dnsServer,
160
+ routeCidrs: [...relayPlan.routes.internalCidrs],
161
+ allowedIps: [...relayPlan.homePeer.allowedIps],
162
+ updatePolicy: product.updatePolicy,
163
+ rateLimitProfile: product.rateLimitProfile,
164
+ dnsPolicyId: product.dnsPolicyId,
165
+ licensePolicyId: product.licensePolicyId,
166
+ snapshotId: snapshot.snapshotId,
167
+ snapshotDigest: snapshot.signatures.digest,
168
+ materialDigest,
169
+ secretUpdatedAt,
170
+ refreshKey: networkRefreshKey(snapshot)
171
+ };
172
+ }
173
+ export function networkDigest(snapshot) {
174
+ return snapshot.signatures.digest;
175
+ }
176
+ export function networkRefreshKey(value) {
177
+ if (typeof value === 'string')
178
+ return value;
179
+ if ('refreshKey' in value)
180
+ return value.refreshKey;
181
+ return [
182
+ value.signatures.digest,
183
+ value.overlayPolicy.productId,
184
+ value.overlayPolicy.launcherMode,
185
+ value.overlayPolicy.identityKind,
186
+ value.overlayPolicy.leaseIp,
187
+ value.topology.product.serviceVip,
188
+ value.topology.internal.relayPeer.fixedIp,
189
+ value.topology.relayPlan.refreshHint.materialDigest ?? '',
190
+ value.topology.relayPlan.refreshHint.secretUpdatedAt ?? ''
191
+ ].join('|');
192
+ }
193
+ export function shouldRefreshNetwork(previous, next) {
194
+ if (!next)
195
+ return false;
196
+ if (!previous)
197
+ return true;
198
+ return networkRefreshKey(previous) !== networkRefreshKey(next);
199
+ }
200
+ export function launcherNetworkScopeForMode(mode) {
201
+ return mode === 'standalone' ? 'owner' : 'broker-session';
202
+ }
203
+ export function createLauncherEmbedManifest(input) {
204
+ const appId = requiredManifestText(input.appId, 'embed appId');
205
+ const productId = optionalManifestText(input.productId) || appId;
206
+ const standaloneChannelProductId = optionalManifestText(input.standaloneChannelProductId) || 'mx-h2i';
207
+ const requiredCapabilities = uniqueLauncherCapabilities(input.requiredCapabilities ?? ['user.session']);
208
+ return {
209
+ appId,
210
+ productId,
211
+ displayName: optionalManifestText(input.displayName),
212
+ description: optionalManifestText(input.description),
213
+ packageName: optionalManifestText(input.packageName),
214
+ category: optionalManifestText(input.category),
215
+ launcherMode: 'embed',
216
+ sdkVersion: optionalManifestText(input.sdkVersion),
217
+ appVersion: optionalManifestText(input.appVersion),
218
+ sdkAbiVersion: optionalManifestText(input.sdkAbiVersion) || brokerAbiVersion,
219
+ protocolVersion: optionalManifestText(input.protocolVersion) || launcherProtocolVersion,
220
+ runtimeContractVersion: optionalManifestText(input.runtimeContractVersion) || launcherEmbedRuntimeContractVersion,
221
+ requiredCapabilities,
222
+ network: {
223
+ scope: launcherNetworkScopeForMode('embed'),
224
+ serviceVip: optionalManifestText(input.serviceVip)
225
+ },
226
+ embed: {
227
+ standaloneChannelProductId,
228
+ launchWithoutBroker: input.launchWithoutBroker === 'prompt-open-standalone' ? 'prompt-open-standalone' : 'blocked'
229
+ }
230
+ };
231
+ }
232
+ export function launcherBrokerCompatibility(input) {
233
+ const embedProtocolMajor = majorVersion(input.embedProtocolVersion ?? launcherProtocolVersion);
234
+ const brokerProtocolMajorValue = majorVersion(input.brokerProtocolVersion ?? launcherProtocolVersion);
235
+ const minBrokerAbiMajor = majorVersion(input.minBrokerAbiVersion ?? brokerAbiVersion);
236
+ const brokerAbiMajorValue = majorVersion(input.brokerAbiVersion ?? brokerAbiVersion);
237
+ if (embedProtocolMajor === null
238
+ || brokerProtocolMajorValue === null
239
+ || minBrokerAbiMajor === null
240
+ || brokerAbiMajorValue === null) {
241
+ return {
242
+ compatible: false,
243
+ reason: 'invalid-version',
244
+ message: 'Launcher protocol or broker ABI version is invalid.'
245
+ };
246
+ }
247
+ if (embedProtocolMajor !== brokerProtocolMajorValue) {
248
+ return {
249
+ compatible: false,
250
+ reason: 'protocol-major-mismatch',
251
+ message: `Embed protocol ${embedProtocolMajor} cannot connect to broker protocol ${brokerProtocolMajorValue}.`
252
+ };
253
+ }
254
+ if (brokerAbiMajorValue < minBrokerAbiMajor) {
255
+ return {
256
+ compatible: false,
257
+ reason: 'broker-abi-too-old',
258
+ message: `Broker ABI ${brokerAbiMajorValue} is older than required ABI ${minBrokerAbiMajor}.`
259
+ };
260
+ }
261
+ return {
262
+ compatible: true,
263
+ reason: 'compatible',
264
+ message: 'Broker protocol and ABI are compatible.'
265
+ };
266
+ }
267
+ export function assertCompatibleBroker(input) {
268
+ const result = launcherBrokerCompatibility(input);
269
+ if (!result.compatible) {
270
+ throw new Error(result.message);
271
+ }
272
+ }
273
+ export function normalizeLauncherBaseUrl(baseUrl) {
274
+ const trimmed = baseUrl.trim();
275
+ if (!trimmed)
276
+ throw new Error('Launcher baseUrl is required');
277
+ return trimmed.replace(/\/+$/, '');
278
+ }
279
+ function joinUrl(baseUrl, path) {
280
+ return `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
281
+ }
282
+ function majorVersion(value) {
283
+ const text = String(value ?? '').trim();
284
+ if (!text)
285
+ return null;
286
+ const match = text.match(/^v?(\d+)/i);
287
+ if (!match)
288
+ return null;
289
+ const major = Number(match[1]);
290
+ return Number.isInteger(major) && major >= 0 ? major : null;
291
+ }
292
+ function requiredManifestText(value, label) {
293
+ const text = optionalManifestText(value);
294
+ if (!text)
295
+ throw new Error(`Launcher manifest ${label} is required`);
296
+ return text;
297
+ }
298
+ function optionalManifestText(value) {
299
+ const text = typeof value === 'string' ? value.trim() : '';
300
+ return text || undefined;
301
+ }
302
+ function uniqueLauncherCapabilities(capabilities) {
303
+ return [...new Set(capabilities.map((capability) => String(capability || '').trim()).filter(Boolean))];
304
+ }
305
+ async function resolveWireGuardKeyMaterial(input) {
306
+ if (input.keyPair) {
307
+ const privateKey = normalizeWireGuardPrivateKey(input.keyPair.privateKey);
308
+ const publicKey = wireGuardPublicKeyFromPrivateKey(privateKey);
309
+ assertMatchingPublicKey(input.keyPair.publicKey, publicKey);
310
+ return { privateKey, publicKey, source: 'provider' };
311
+ }
312
+ if (input.keyProvider) {
313
+ const keyPair = await input.keyProvider();
314
+ const privateKey = normalizeWireGuardPrivateKey(keyPair.privateKey);
315
+ const publicKey = wireGuardPublicKeyFromPrivateKey(privateKey);
316
+ assertMatchingPublicKey(keyPair.publicKey, publicKey);
317
+ return { privateKey, publicKey, source: 'provider' };
318
+ }
319
+ if (input.privateKey) {
320
+ const privateKey = normalizeWireGuardPrivateKey(input.privateKey);
321
+ const publicKey = wireGuardPublicKeyFromPrivateKey(privateKey);
322
+ if (input.publicKey)
323
+ assertMatchingPublicKey(input.publicKey, publicKey);
324
+ return { privateKey, publicKey, source: 'provided-private-key' };
325
+ }
326
+ if (input.publicKey) {
327
+ return {
328
+ privateKey: null,
329
+ publicKey: normalizeWireGuardPublicKey(input.publicKey),
330
+ source: 'provided-public-key'
331
+ };
332
+ }
333
+ const keyPair = createLauncherWireGuardKeyPair();
334
+ return {
335
+ privateKey: keyPair.privateKey,
336
+ publicKey: keyPair.publicKey,
337
+ source: 'generated'
338
+ };
339
+ }
340
+ function launcherProductMode(value) {
341
+ return value === 'standalone' ? 'standalone' : 'embed';
342
+ }
343
+ function randomWireGuardPrivateKey() {
344
+ return bytesToBase64(clampWireGuardPrivateKey(randomBytes32()));
345
+ }
346
+ function normalizeWireGuardPrivateKey(value) {
347
+ return bytesToBase64(clampWireGuardPrivateKey(decodeBase64Key(value, 'WireGuard private key')));
348
+ }
349
+ function normalizeWireGuardPublicKey(value) {
350
+ return bytesToBase64(decodeBase64Key(value, 'WireGuard public key'));
351
+ }
352
+ function assertMatchingPublicKey(provided, expected) {
353
+ if (!provided)
354
+ return;
355
+ if (normalizeWireGuardPublicKey(provided) !== expected) {
356
+ throw new Error('WireGuard publicKey does not match privateKey');
357
+ }
358
+ }
359
+ function randomBytes32() {
360
+ const cryptoLike = globalThis.crypto;
361
+ if (!cryptoLike?.getRandomValues) {
362
+ throw new Error('Secure random generator is not available for WireGuard key generation');
363
+ }
364
+ return cryptoLike.getRandomValues(new Uint8Array(32));
365
+ }
366
+ function decodeBase64Key(value, label) {
367
+ const trimmed = value.trim();
368
+ const bufferFactory = globalThis.Buffer;
369
+ if (bufferFactory?.from) {
370
+ const buffer = bufferFactory.from(trimmed, 'base64');
371
+ const bytes = new Uint8Array(buffer.length);
372
+ for (let index = 0; index < buffer.length; index += 1)
373
+ bytes[index] = buffer[index];
374
+ if (bytes.length !== 32)
375
+ throw new Error(`${label} must decode to 32 bytes`);
376
+ return bytes;
377
+ }
378
+ const atobFn = globalThis.atob;
379
+ if (!atobFn)
380
+ throw new Error('Base64 decoder is not available');
381
+ const binary = atobFn(trimmed);
382
+ const bytes = new Uint8Array(binary.length);
383
+ for (let index = 0; index < binary.length; index += 1)
384
+ bytes[index] = binary.charCodeAt(index);
385
+ if (bytes.length !== 32)
386
+ throw new Error(`${label} must decode to 32 bytes`);
387
+ return bytes;
388
+ }
389
+ function bytesToBase64(bytes) {
390
+ const bufferFactory = globalThis.Buffer;
391
+ if (bufferFactory?.from)
392
+ return bufferFactory.from(bytes).toString('base64');
393
+ const btoaFn = globalThis.btoa;
394
+ if (!btoaFn)
395
+ throw new Error('Base64 encoder is not available');
396
+ return btoaFn(String.fromCharCode(...bytes));
397
+ }
398
+ function clampWireGuardPrivateKey(bytes) {
399
+ if (bytes.length !== 32)
400
+ throw new Error('WireGuard private key must be 32 bytes');
401
+ const next = new Uint8Array(bytes);
402
+ next[0] &= 248;
403
+ next[31] &= 127;
404
+ next[31] |= 64;
405
+ return next;
406
+ }
407
+ function basePointBytes() {
408
+ const bytes = new Uint8Array(32);
409
+ bytes[0] = 9;
410
+ return bytes;
411
+ }
412
+ function x25519(scalar, point) {
413
+ const p = (1n << 255n) - 19n;
414
+ const x1 = decodeLittleEndian(point);
415
+ let x2 = 1n;
416
+ let z2 = 0n;
417
+ let x3 = x1;
418
+ let z3 = 1n;
419
+ let swap = 0n;
420
+ const k = decodeLittleEndian(clampWireGuardPrivateKey(scalar));
421
+ for (let t = 254; t >= 0; t -= 1) {
422
+ const bit = (k >> BigInt(t)) & 1n;
423
+ swap ^= bit;
424
+ [x2, x3] = conditionalSwap(swap, x2, x3);
425
+ [z2, z3] = conditionalSwap(swap, z2, z3);
426
+ swap = bit;
427
+ const a = mod(x2 + z2, p);
428
+ const aa = mod(a * a, p);
429
+ const b = mod(x2 - z2, p);
430
+ const bb = mod(b * b, p);
431
+ const e = mod(aa - bb, p);
432
+ const c = mod(x3 + z3, p);
433
+ const d = mod(x3 - z3, p);
434
+ const da = mod(d * a, p);
435
+ const cb = mod(c * b, p);
436
+ x3 = mod((da + cb) * (da + cb), p);
437
+ z3 = mod(x1 * mod((da - cb) * (da - cb), p), p);
438
+ x2 = mod(aa * bb, p);
439
+ z2 = mod(e * (aa + 121665n * e), p);
440
+ }
441
+ [x2, x3] = conditionalSwap(swap, x2, x3);
442
+ [z2, z3] = conditionalSwap(swap, z2, z3);
443
+ return encodeLittleEndian(mod(x2 * modInverse(z2, p), p), 32);
444
+ }
445
+ function conditionalSwap(swap, left, right) {
446
+ return swap === 1n ? [right, left] : [left, right];
447
+ }
448
+ function decodeLittleEndian(bytes) {
449
+ let value = 0n;
450
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
451
+ value = (value << 8n) + BigInt(bytes[index]);
452
+ }
453
+ return value;
454
+ }
455
+ function encodeLittleEndian(value, length) {
456
+ const bytes = new Uint8Array(length);
457
+ let next = value;
458
+ for (let index = 0; index < length; index += 1) {
459
+ bytes[index] = Number(next & 255n);
460
+ next >>= 8n;
461
+ }
462
+ return bytes;
463
+ }
464
+ function mod(value, p) {
465
+ const result = value % p;
466
+ return result >= 0n ? result : result + p;
467
+ }
468
+ function modInverse(value, p) {
469
+ return modPow(value, p - 2n, p);
470
+ }
471
+ function modPow(base, exponent, p) {
472
+ let result = 1n;
473
+ let nextBase = mod(base, p);
474
+ let nextExponent = exponent;
475
+ while (nextExponent > 0n) {
476
+ if (nextExponent & 1n)
477
+ result = mod(result * nextBase, p);
478
+ nextBase = mod(nextBase * nextBase, p);
479
+ nextExponent >>= 1n;
480
+ }
481
+ return result;
482
+ }
483
+ function compactBody(input) {
484
+ const body = {};
485
+ for (const [key, value] of Object.entries(input)) {
486
+ if (value !== undefined && value !== null)
487
+ body[key] = value;
488
+ }
489
+ return body;
490
+ }
491
+ async function requestJson(fetchImpl, url, method, body) {
492
+ const response = await fetchImpl(url, {
493
+ method,
494
+ headers: {
495
+ accept: 'application/json',
496
+ ...(body ? { 'content-type': 'application/json' } : {})
497
+ },
498
+ ...(body ? { body: JSON.stringify(body) } : {})
499
+ });
500
+ const text = await response.text();
501
+ const payload = parseJsonPayload(text);
502
+ if (!response.ok) {
503
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
504
+ throw new LauncherApiError(`MX Launcher request failed: ${response.status}${statusText}`, response.status, payload);
505
+ }
506
+ return payload;
507
+ }
508
+ function parseJsonPayload(text) {
509
+ if (!text.trim())
510
+ return null;
511
+ try {
512
+ return JSON.parse(text);
513
+ }
514
+ catch {
515
+ return text;
516
+ }
517
+ }
518
+ function getGlobalFetch() {
519
+ const globalWithFetch = globalThis;
520
+ if (!globalWithFetch.fetch)
521
+ throw new Error('No fetch implementation available for MX Launcher client');
522
+ return globalWithFetch.fetch.bind(globalThis);
523
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@qpjoy/mx-launcher-core",
3
+ "version": "0.2.0",
4
+ "description": "Shared MX Launcher core for product network enroll, snapshots, routes, DNS, license, and rate-limit policy contracts.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "package.json"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.7.3"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit"
28
+ }
29
+ }