@agentunion/fastaun-browser 0.4.9 → 0.4.11
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/CHANGELOG.md +46 -0
- package/_packed_docs/CHANGELOG.md +46 -0
- package/_packed_docs/INDEX.md +31 -14
- package/_packed_docs/KITE_DOCS_GUIDE.md +20 -14
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +244 -16
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +114 -28
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +7 -4
- package/_packed_docs/sdk/09-group-rpc-manual.md +238 -2
- package/_packed_docs/sdk/09-proxy-rpc-manual.md +231 -0
- package/_packed_docs/sdk/09-storage-rpc-manual.md +354 -22
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +15 -11
- package/_packed_docs/sdk/INDEX.md +14 -8
- package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +214 -0
- package/_packed_docs/sdk/README.md +8 -6
- package/dist/bundle.js +1611 -48
- package/dist/client/delivery.d.ts +8 -1
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +241 -15
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +2 -2
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +29 -4
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +16 -2
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +131 -14
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/service-proxy.d.ts +219 -0
- package/dist/service-proxy.d.ts.map +1 -0
- package/dist/service-proxy.js +1321 -0
- package/dist/service-proxy.js.map +1 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +34 -0
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/session/keystore.d.ts.map +1 -1
- package/dist/v2/session/keystore.js +8 -9
- package/dist/v2/session/keystore.js.map +1 -1
- package/dist/v2/session/session.d.ts +4 -2
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +20 -4
- package/dist/v2/session/session.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -454,7 +454,7 @@ var init_indexeddb_store = __esm({
|
|
|
454
454
|
});
|
|
455
455
|
|
|
456
456
|
// src/version.ts
|
|
457
|
-
var VERSION = "0.4.
|
|
457
|
+
var VERSION = "0.4.11";
|
|
458
458
|
|
|
459
459
|
// src/types.ts
|
|
460
460
|
var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
|
|
@@ -1236,6 +1236,7 @@ var EVENT_NAME_MAP = {
|
|
|
1236
1236
|
"message.ack": "message.ack",
|
|
1237
1237
|
"group.changed": "group.changed",
|
|
1238
1238
|
"group.message_created": "group.message_created",
|
|
1239
|
+
"group.message_recalled": "group.message_recalled",
|
|
1239
1240
|
"group.state_committed": "group.state_committed",
|
|
1240
1241
|
"storage.object_changed": "storage.object_changed"
|
|
1241
1242
|
};
|
|
@@ -1579,6 +1580,34 @@ var RPCTransport = class {
|
|
|
1579
1580
|
this._drainRpcQueue();
|
|
1580
1581
|
});
|
|
1581
1582
|
}
|
|
1583
|
+
/** 发送 JSON-RPC 2.0 Notification,不分配 id,也不等待响应。 */
|
|
1584
|
+
async notify(method, params) {
|
|
1585
|
+
if (this._closed || !this._ws) {
|
|
1586
|
+
throw this._notConnectedError();
|
|
1587
|
+
}
|
|
1588
|
+
const normalizedMethod = String(method ?? "").trim();
|
|
1589
|
+
if (!normalizedMethod.startsWith("notification/") && !normalizedMethod.startsWith("event/")) {
|
|
1590
|
+
throw new ValidationError("notify method must start with notification/ or event/");
|
|
1591
|
+
}
|
|
1592
|
+
if (params !== void 0 && params !== null && !isJsonObject(params)) {
|
|
1593
|
+
throw new ValidationError("notify params must be an object");
|
|
1594
|
+
}
|
|
1595
|
+
const payload = JSON.stringify({
|
|
1596
|
+
jsonrpc: "2.0",
|
|
1597
|
+
method: normalizedMethod,
|
|
1598
|
+
params: params ?? {}
|
|
1599
|
+
});
|
|
1600
|
+
const payloadSize = new TextEncoder().encode(payload).length;
|
|
1601
|
+
if (payloadSize > MAX_WS_PAYLOAD_SIZE) {
|
|
1602
|
+
throw new ValidationError("payload is too large");
|
|
1603
|
+
}
|
|
1604
|
+
try {
|
|
1605
|
+
this._ws.send(payload);
|
|
1606
|
+
this._log.debug(`notification sent: method=${normalizedMethod}, size=${payloadSize}`);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
throw new ConnectionError(`failed to send notification ${normalizedMethod}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1582
1611
|
/** 从 pending / queue 中移除指定 RPC */
|
|
1583
1612
|
_removeRpc(rpcId, pending) {
|
|
1584
1613
|
const current = this._pending.get(rpcId);
|
|
@@ -1747,6 +1776,10 @@ var RPCTransport = class {
|
|
|
1747
1776
|
this._log.info(`[trace=${String(traceObj.trace_id ?? "")}] event_recv event=${sdkEvent}`);
|
|
1748
1777
|
}
|
|
1749
1778
|
}
|
|
1779
|
+
if (sdkEvent.startsWith("app.")) {
|
|
1780
|
+
this._dispatcher.publish(sdkEvent, params);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1750
1783
|
this._dispatcher.publish(`_raw.${sdkEvent}`, params);
|
|
1751
1784
|
return;
|
|
1752
1785
|
}
|
|
@@ -3676,6 +3709,7 @@ var ClientRuntime = class {
|
|
|
3676
3709
|
// src/client/delivery.ts
|
|
3677
3710
|
var PUSHED_SEQS_LIMIT = 5e4;
|
|
3678
3711
|
var PENDING_ORDERED_LIMIT = 5e4;
|
|
3712
|
+
var GROUP_RECALL_SEEN_LIMIT = 1e4;
|
|
3679
3713
|
function formatDeliveryError(error) {
|
|
3680
3714
|
return error instanceof Error ? error : String(error);
|
|
3681
3715
|
}
|
|
@@ -3719,8 +3753,31 @@ var MessageDeliveryEngine = class {
|
|
|
3719
3753
|
for (const oldSeq of drop) queue.delete(oldSeq);
|
|
3720
3754
|
}
|
|
3721
3755
|
}
|
|
3756
|
+
isGroupEventNamespace(ns) {
|
|
3757
|
+
return ns.startsWith("group_event:");
|
|
3758
|
+
}
|
|
3759
|
+
async publishOrderedQueueItem(ns, event, seq, payload) {
|
|
3760
|
+
const client = this.runtime.client;
|
|
3761
|
+
if (event === "group.changed" && this.isGroupEventNamespace(ns)) {
|
|
3762
|
+
await this.publishOrderedGroupChanged(payload);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
await client._publishAppEvent(event, payload);
|
|
3766
|
+
}
|
|
3767
|
+
async publishOrderedGroupChanged(payload) {
|
|
3768
|
+
const client = this.runtime.client;
|
|
3769
|
+
if (isJsonObject(payload)) {
|
|
3770
|
+
const eventPayload = payload;
|
|
3771
|
+
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
3772
|
+
if (eventPayload.action === "dissolved") {
|
|
3773
|
+
const groupId = normalizeGroupId(String(eventPayload.group_id ?? "")) || String(eventPayload.group_id ?? "").trim();
|
|
3774
|
+
if (groupId) client._cleanupDissolvedGroup?.(groupId);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
await client._publishAppEvent("group.changed", payload);
|
|
3778
|
+
}
|
|
3722
3779
|
isInstanceScopedMessageEvent(event) {
|
|
3723
|
-
return event === "message.received" || event === "message.recalled" || event === "message.undecryptable" || event === "group.message_created" || event === "group.message_undecryptable";
|
|
3780
|
+
return event === "message.received" || event === "message.recalled" || event === "message.undecryptable" || event === "group.message_created" || event === "group.message_recalled" || event === "group.message_undecryptable";
|
|
3724
3781
|
}
|
|
3725
3782
|
attachCurrentInstanceContext(payload) {
|
|
3726
3783
|
if (!isJsonObject(payload)) return payload;
|
|
@@ -3784,6 +3841,121 @@ var MessageDeliveryEngine = class {
|
|
|
3784
3841
|
if (recall) return { event: "message.recalled", payload: recall };
|
|
3785
3842
|
return { event: "message.received", payload: message };
|
|
3786
3843
|
}
|
|
3844
|
+
recallEventFromGroupMessage(message) {
|
|
3845
|
+
if (!isJsonObject(message)) return null;
|
|
3846
|
+
const msg = message;
|
|
3847
|
+
const rawPayload = msg.payload;
|
|
3848
|
+
const payload = isJsonObject(rawPayload) ? rawPayload : {};
|
|
3849
|
+
const msgType = String(msg.type ?? msg.kind ?? msg.message_type ?? "").trim();
|
|
3850
|
+
const payloadType = String(payload.type ?? payload.kind ?? "").trim();
|
|
3851
|
+
if (msgType !== "group.message_recalled" && payloadType !== "group.message_recalled") return null;
|
|
3852
|
+
const event = { ...payload };
|
|
3853
|
+
const rawIds = event.message_ids;
|
|
3854
|
+
let messageIds = Array.isArray(rawIds) ? rawIds.map((item) => String(item ?? "").trim()).filter(Boolean) : [];
|
|
3855
|
+
if (messageIds.length === 0) {
|
|
3856
|
+
for (const key of ["recalled_message_id", "target_message_id", "original_message_id"]) {
|
|
3857
|
+
const value = String(event[key] ?? "").trim();
|
|
3858
|
+
if (value) {
|
|
3859
|
+
messageIds = [value];
|
|
3860
|
+
break;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
event.type = "group.message_recalled";
|
|
3865
|
+
event.kind = "group.message_recalled";
|
|
3866
|
+
event.message_ids = messageIds;
|
|
3867
|
+
if (!("group_id" in event)) event.group_id = msg.group_id ?? "";
|
|
3868
|
+
if (!("timestamp" in event)) event.timestamp = msg.timestamp ?? msg.t_server ?? event.recalled_at ?? 0;
|
|
3869
|
+
if ("seq" in msg) event.seq = msg.seq;
|
|
3870
|
+
if ("message_id" in msg && !("tombstone_message_id" in event)) event.tombstone_message_id = msg.message_id;
|
|
3871
|
+
return event;
|
|
3872
|
+
}
|
|
3873
|
+
groupRecallDedupKey(groupId, payload) {
|
|
3874
|
+
const ids = payload.message_ids;
|
|
3875
|
+
const idPart = Array.isArray(ids) ? ids.map((i) => String(i ?? "").trim()).filter(Boolean).sort().join(",") : String(ids ?? "");
|
|
3876
|
+
return `${groupId}|${idPart}`;
|
|
3877
|
+
}
|
|
3878
|
+
async publishGroupRecallTombstone(groupId, seq, message) {
|
|
3879
|
+
const client = this.runtime.client;
|
|
3880
|
+
const eventPayload = this.recallEventFromGroupMessage(message);
|
|
3881
|
+
if (!eventPayload) return false;
|
|
3882
|
+
const dedupKey = this.groupRecallDedupKey(groupId, eventPayload);
|
|
3883
|
+
let seen = client._groupRecallSeen;
|
|
3884
|
+
if (!seen) {
|
|
3885
|
+
seen = /* @__PURE__ */ new Map();
|
|
3886
|
+
client._groupRecallSeen = seen;
|
|
3887
|
+
}
|
|
3888
|
+
if (seen.has(dedupKey)) {
|
|
3889
|
+
client._clientLog.debug(`group.message_recalled dedup suppressed: group=${groupId} seq=${String(seq)} key=${dedupKey}`);
|
|
3890
|
+
return false;
|
|
3891
|
+
}
|
|
3892
|
+
seen.set(dedupKey, Date.now());
|
|
3893
|
+
if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
|
|
3894
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
|
|
3895
|
+
for (const [oldKey] of drop) seen.delete(oldKey);
|
|
3896
|
+
}
|
|
3897
|
+
await client._publishAppEvent("group.message_recalled", eventPayload);
|
|
3898
|
+
client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
3899
|
+
return true;
|
|
3900
|
+
}
|
|
3901
|
+
async onRawGroupMessageRecalled(data) {
|
|
3902
|
+
const client = this.runtime.client;
|
|
3903
|
+
if (!isJsonObject(data)) return;
|
|
3904
|
+
const src = data;
|
|
3905
|
+
const groupId = String(src.group_id ?? "").trim();
|
|
3906
|
+
const wrapped = { ...src };
|
|
3907
|
+
if (!("type" in wrapped)) wrapped.type = "group.message_recalled";
|
|
3908
|
+
if (!("payload" in wrapped)) {
|
|
3909
|
+
wrapped.payload = {
|
|
3910
|
+
type: "group.message_recalled",
|
|
3911
|
+
message_ids: src.message_ids ?? [],
|
|
3912
|
+
target_message_seqs: src.target_message_seqs ?? [],
|
|
3913
|
+
sender_aid: src.sender_aid ?? "",
|
|
3914
|
+
recalled_by: src.recalled_by ?? "",
|
|
3915
|
+
recalled_at: src.recalled_at ?? src.timestamp ?? 0,
|
|
3916
|
+
reason: src.reason ?? "",
|
|
3917
|
+
group_id: groupId
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
const seq = src.seq;
|
|
3921
|
+
const seqNum = Number(seq);
|
|
3922
|
+
if (!groupId || seq === void 0 || seq === null || !Number.isFinite(seqNum) || !Number.isInteger(seqNum)) {
|
|
3923
|
+
await this.publishGroupRecallTombstone(groupId, seq, wrapped);
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
const ns = `group:${groupId}`;
|
|
3927
|
+
if (seqNum > 0) {
|
|
3928
|
+
client._seqTracker.updateMaxSeen(ns, seqNum);
|
|
3929
|
+
if (client._seqTracker.getContiguousSeq(ns) === seqNum) {
|
|
3930
|
+
await this.publishGroupRecallTombstone(groupId, seq, wrapped);
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
client._repairPushContiguousBound(ns, seqNum, true, "_raw.group.message_recalled");
|
|
3934
|
+
}
|
|
3935
|
+
const pushed = client._pushedSeqs.get(ns);
|
|
3936
|
+
const pending = client._pendingOrderedMsgs.get(ns);
|
|
3937
|
+
if (pushed?.has(seqNum) || pending?.has(seqNum)) {
|
|
3938
|
+
await this.publishGroupRecallTombstone(groupId, seq, wrapped);
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3941
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
3942
|
+
client._seqTracker.onMessageSeq(ns, seqNum);
|
|
3943
|
+
await this.publishGroupRecallTombstone(groupId, seq, wrapped);
|
|
3944
|
+
this.markPublishedSeq(ns, seqNum);
|
|
3945
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
3946
|
+
if (contig > 0) {
|
|
3947
|
+
const ackSeq = this.clampAckSeq("group.ack_messages", "msg_seq", ns, contig);
|
|
3948
|
+
client._transport.call("group.ack_messages", {
|
|
3949
|
+
group_id: groupId,
|
|
3950
|
+
msg_seq: ackSeq,
|
|
3951
|
+
device_id: client._deviceId,
|
|
3952
|
+
slot_id: client._slotId
|
|
3953
|
+
}).catch((e) => {
|
|
3954
|
+
client._clientLog.warn("group recall auto-ack failed: group=" + groupId, e);
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
if (contig !== contigBefore) this.saveSeqTrackerState();
|
|
3958
|
+
}
|
|
3787
3959
|
async publishAppEvent(event, payload) {
|
|
3788
3960
|
const client = this.runtime.client;
|
|
3789
3961
|
if ((event === "message.received" || event === "group.message_created") && isJsonObject(payload)) {
|
|
@@ -3920,6 +4092,25 @@ var MessageDeliveryEngine = class {
|
|
|
3920
4092
|
if (seq > 0) client._seqTracker.updateMaxSeen(ns, seq);
|
|
3921
4093
|
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
3922
4094
|
const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
|
|
4095
|
+
if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
|
|
4096
|
+
await this.publishGroupRecallTombstone(groupId, seq, msg);
|
|
4097
|
+
this.markPublishedSeq(ns, Number(seq));
|
|
4098
|
+
const contigAfter2 = client._seqTracker.getContiguousSeq(ns);
|
|
4099
|
+
const contig2 = client._seqTracker.getContiguousSeq(ns);
|
|
4100
|
+
if (contig2 > 0) {
|
|
4101
|
+
const ackSeq = this.clampAckSeq("group.ack_messages", "msg_seq", ns, contig2);
|
|
4102
|
+
client._transport.call("group.ack_messages", {
|
|
4103
|
+
group_id: groupId,
|
|
4104
|
+
msg_seq: ackSeq,
|
|
4105
|
+
device_id: client._deviceId,
|
|
4106
|
+
slot_id: client._slotId
|
|
4107
|
+
}).catch((e) => {
|
|
4108
|
+
client._clientLog.warn("group recall auto-ack failed: group=" + groupId, e);
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
if (contigAfter2 !== contigBefore) this.saveSeqTrackerState();
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
3923
4114
|
const published = encryptedPush ? await client._publishEncryptedPushMessage("group.message_created", "group.message_undecryptable", ns, seq, msg, true) : await this.publishOrderedMessage("group.message_created", ns, seq, msg);
|
|
3924
4115
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
3925
4116
|
const needPull = seqNeedsPull && !published;
|
|
@@ -3994,6 +4185,11 @@ var MessageDeliveryEngine = class {
|
|
|
3994
4185
|
if (pushed && s !== void 0 && s !== null && pushed.has(s)) {
|
|
3995
4186
|
continue;
|
|
3996
4187
|
}
|
|
4188
|
+
if (s !== void 0 && s !== null && this.recallEventFromGroupMessage(msg)) {
|
|
4189
|
+
await this.publishGroupRecallTombstone(groupId, s, msg);
|
|
4190
|
+
this.markPublishedSeq(ns, Number(s));
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
3997
4193
|
if (s !== void 0 && s !== null) {
|
|
3998
4194
|
await client._publishPulledMessage("group.message_created", ns, s, msg);
|
|
3999
4195
|
} else {
|
|
@@ -4100,10 +4296,12 @@ var MessageDeliveryEngine = class {
|
|
|
4100
4296
|
}
|
|
4101
4297
|
}
|
|
4102
4298
|
const eventSeqs = [];
|
|
4299
|
+
let hasDissolvedEvent = false;
|
|
4103
4300
|
for (const evt of eventObjects) {
|
|
4104
4301
|
const eventSeq = Number(evt.event_seq ?? 0);
|
|
4105
4302
|
if (Number.isFinite(eventSeq) && eventSeq > 0) eventSeqs.push(eventSeq);
|
|
4106
4303
|
evt._from_gap_fill = true;
|
|
4304
|
+
if (evt.action === "dissolved") hasDissolvedEvent = true;
|
|
4107
4305
|
const et = String(evt.event_type ?? "");
|
|
4108
4306
|
if (et === "group.message_created") continue;
|
|
4109
4307
|
const cs = evt.client_signature;
|
|
@@ -4111,17 +4309,21 @@ var MessageDeliveryEngine = class {
|
|
|
4111
4309
|
if (client._shouldSkipEventSignature(evt)) {
|
|
4112
4310
|
delete evt.client_signature;
|
|
4113
4311
|
} else {
|
|
4114
|
-
|
|
4312
|
+
const verified = await client._verifyEventSignature(evt, cs);
|
|
4313
|
+
evt._verified = client._isEventSignatureVerified(verified);
|
|
4115
4314
|
}
|
|
4116
4315
|
}
|
|
4117
|
-
|
|
4316
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
|
|
4317
|
+
this.enqueueOrderedMessage(ns, "group.changed", eventSeq, evt);
|
|
4318
|
+
}
|
|
4118
4319
|
}
|
|
4119
|
-
const
|
|
4120
|
-
|
|
4320
|
+
const ackContig = client._seqTracker.getContiguousSeq(ns);
|
|
4321
|
+
await this.drainOrderedMessages(ns);
|
|
4322
|
+
if (ackContig !== pageContigBefore && !hasDissolvedEvent) {
|
|
4121
4323
|
this.saveSeqTrackerState();
|
|
4122
4324
|
}
|
|
4123
|
-
if (eventObjects.length > 0 &&
|
|
4124
|
-
const ackSeq = this.clampAckSeq("group.ack_events", "event_seq", ns,
|
|
4325
|
+
if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
|
|
4326
|
+
const ackSeq = this.clampAckSeq("group.ack_events", "event_seq", ns, ackContig);
|
|
4125
4327
|
client._transport.call("group.ack_events", {
|
|
4126
4328
|
group_id: groupId,
|
|
4127
4329
|
event_seq: ackSeq,
|
|
@@ -4145,15 +4347,36 @@ var MessageDeliveryEngine = class {
|
|
|
4145
4347
|
this.runtime.delivery.setGapFillActive(false);
|
|
4146
4348
|
}
|
|
4147
4349
|
}
|
|
4148
|
-
handleGroupChangedEventSeq(data, groupId) {
|
|
4350
|
+
async handleGroupChangedEventSeq(data, groupId) {
|
|
4149
4351
|
const client = this.runtime.client;
|
|
4150
4352
|
let needPull = false;
|
|
4151
4353
|
const rawEventSeq = data.event_seq;
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4354
|
+
const eventSeq = Number(rawEventSeq);
|
|
4355
|
+
if (!groupId || !Number.isFinite(eventSeq) || !Number.isInteger(eventSeq) || eventSeq <= 0) {
|
|
4356
|
+
await this.publishOrderedGroupChanged(data);
|
|
4357
|
+
return;
|
|
4358
|
+
}
|
|
4359
|
+
const ns = `group_event:${groupId}`;
|
|
4360
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
4361
|
+
if (eventSeq <= contigBefore || client._pushedSeqs.get(ns)?.has(eventSeq)) {
|
|
4362
|
+
client._clientLog.debug(`group.changed skipped duplicate/stale: group=${groupId}, event_seq=${eventSeq}, contiguous=${contigBefore}`);
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
this.enqueueOrderedMessage(ns, "group.changed", eventSeq, data);
|
|
4366
|
+
needPull = client._seqTracker.onMessageSeq(ns, eventSeq);
|
|
4367
|
+
const ackContig = client._seqTracker.getContiguousSeq(ns);
|
|
4368
|
+
await this.drainOrderedMessages(ns);
|
|
4369
|
+
if (ackContig > 0 && ackContig !== contigBefore) {
|
|
4370
|
+
if (data.action !== "dissolved") this.saveSeqTrackerState();
|
|
4371
|
+
const ackSeq = this.clampAckSeq("group.ack_events", "event_seq", ns, ackContig);
|
|
4372
|
+
client._transport.call("group.ack_events", {
|
|
4373
|
+
group_id: groupId,
|
|
4374
|
+
event_seq: ackSeq,
|
|
4375
|
+
device_id: client._deviceId,
|
|
4376
|
+
slot_id: client._slotId
|
|
4377
|
+
}).catch((e) => {
|
|
4378
|
+
client._clientLog.warn("group event push auto-ack failed: group=" + groupId, e);
|
|
4379
|
+
});
|
|
4157
4380
|
}
|
|
4158
4381
|
if (needPull && groupId && !data._from_gap_fill) {
|
|
4159
4382
|
client._safeAsync(this.fillGroupEventGap(groupId));
|
|
@@ -4577,7 +4800,7 @@ var MessageDeliveryEngine = class {
|
|
|
4577
4800
|
const item = queue.get(seq);
|
|
4578
4801
|
queue.delete(seq);
|
|
4579
4802
|
if (!item || client._pushedSeqs.get(ns)?.has(seq)) continue;
|
|
4580
|
-
await
|
|
4803
|
+
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload);
|
|
4581
4804
|
this.markPublishedSeq(ns, seq);
|
|
4582
4805
|
}
|
|
4583
4806
|
if (queue.size === 0) client._pendingOrderedMsgs.delete(ns);
|
|
@@ -4586,7 +4809,7 @@ var MessageDeliveryEngine = class {
|
|
|
4586
4809
|
const client = this.runtime.client;
|
|
4587
4810
|
const seqNum = Number(seq);
|
|
4588
4811
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
4589
|
-
await
|
|
4812
|
+
await this.publishOrderedQueueItem(ns, event, seqNum, payload);
|
|
4590
4813
|
return true;
|
|
4591
4814
|
}
|
|
4592
4815
|
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
@@ -4605,7 +4828,7 @@ var MessageDeliveryEngine = class {
|
|
|
4605
4828
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
4606
4829
|
queue?.delete(seqNum);
|
|
4607
4830
|
if (queue && queue.size === 0) client._pendingOrderedMsgs.delete(ns);
|
|
4608
|
-
await
|
|
4831
|
+
await this.publishOrderedQueueItem(ns, event, seqNum, payload);
|
|
4609
4832
|
this.markPublishedSeq(ns, seqNum);
|
|
4610
4833
|
await this.drainOrderedMessages(ns);
|
|
4611
4834
|
return true;
|
|
@@ -5000,12 +5223,37 @@ var SIGNED_METHODS = /* @__PURE__ */ new Set([
|
|
|
5000
5223
|
"message.thought.put",
|
|
5001
5224
|
"group.set_settings",
|
|
5002
5225
|
"group.resources.put",
|
|
5226
|
+
"group.resources.create_folder",
|
|
5227
|
+
"group.resources.rename",
|
|
5228
|
+
"group.resources.move",
|
|
5229
|
+
"group.resources.mount_object",
|
|
5003
5230
|
"group.resources.update",
|
|
5004
5231
|
"group.resources.delete",
|
|
5232
|
+
"group.resources.cleanup_by_storage_ref",
|
|
5005
5233
|
"group.resources.request_add",
|
|
5234
|
+
"group.resources.request_mount_object",
|
|
5006
5235
|
"group.resources.direct_add",
|
|
5007
5236
|
"group.resources.approve_request",
|
|
5008
5237
|
"group.resources.reject_request",
|
|
5238
|
+
"group.resources.unmount",
|
|
5239
|
+
"group.resources.get_access",
|
|
5240
|
+
"group.resources.resolve_access_ticket",
|
|
5241
|
+
"storage.put_object",
|
|
5242
|
+
"storage.delete_object",
|
|
5243
|
+
"storage.get_by_share",
|
|
5244
|
+
"storage.create_share_link",
|
|
5245
|
+
"storage.revoke_share_link",
|
|
5246
|
+
"storage.create_upload_session",
|
|
5247
|
+
"storage.complete_upload",
|
|
5248
|
+
"storage.create_folder",
|
|
5249
|
+
"storage.rename_folder",
|
|
5250
|
+
"storage.move_folder",
|
|
5251
|
+
"storage.delete_folder",
|
|
5252
|
+
"storage.move_object",
|
|
5253
|
+
"storage.copy_object",
|
|
5254
|
+
"storage.batch_delete",
|
|
5255
|
+
"storage.set_object_meta",
|
|
5256
|
+
"storage.append_object",
|
|
5009
5257
|
"group.commit_state",
|
|
5010
5258
|
"group.ban",
|
|
5011
5259
|
"group.unban",
|
|
@@ -5028,15 +5276,44 @@ var NON_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
|
|
|
5028
5276
|
"group.update_avatar",
|
|
5029
5277
|
"group.update_announcement",
|
|
5030
5278
|
"group.update_settings",
|
|
5031
|
-
"storage.
|
|
5279
|
+
"storage.put_object",
|
|
5280
|
+
"storage.delete_object",
|
|
5281
|
+
"storage.create_share_link",
|
|
5282
|
+
"storage.revoke_share_link",
|
|
5283
|
+
"storage.get_by_share",
|
|
5284
|
+
"storage.create_upload_session",
|
|
5032
5285
|
"storage.complete_upload",
|
|
5033
|
-
"storage.
|
|
5286
|
+
"storage.create_folder",
|
|
5287
|
+
"storage.rename_folder",
|
|
5288
|
+
"storage.move_folder",
|
|
5289
|
+
"storage.delete_folder",
|
|
5290
|
+
"storage.move_object",
|
|
5291
|
+
"storage.copy_object",
|
|
5292
|
+
"storage.batch_delete",
|
|
5293
|
+
"storage.set_object_meta",
|
|
5294
|
+
"storage.append_object",
|
|
5034
5295
|
"auth.create_aid",
|
|
5035
5296
|
"auth.renew_cert",
|
|
5036
5297
|
"auth.rekey",
|
|
5037
5298
|
"message.thought.put",
|
|
5038
5299
|
"group.thought.put",
|
|
5039
|
-
"group.add_member"
|
|
5300
|
+
"group.add_member",
|
|
5301
|
+
"group.resources.put",
|
|
5302
|
+
"group.resources.create_folder",
|
|
5303
|
+
"group.resources.rename",
|
|
5304
|
+
"group.resources.move",
|
|
5305
|
+
"group.resources.mount_object",
|
|
5306
|
+
"group.resources.update",
|
|
5307
|
+
"group.resources.delete",
|
|
5308
|
+
"group.resources.cleanup_by_storage_ref",
|
|
5309
|
+
"group.resources.request_add",
|
|
5310
|
+
"group.resources.request_mount_object",
|
|
5311
|
+
"group.resources.direct_add",
|
|
5312
|
+
"group.resources.approve_request",
|
|
5313
|
+
"group.resources.reject_request",
|
|
5314
|
+
"group.resources.unmount",
|
|
5315
|
+
"group.resources.get_access",
|
|
5316
|
+
"group.resources.resolve_access_ticket"
|
|
5040
5317
|
]);
|
|
5041
5318
|
var RpcPipeline = class {
|
|
5042
5319
|
constructor(runtime) {
|
|
@@ -8497,7 +8774,7 @@ function normalizeProtectedHeaders(headers, payload) {
|
|
|
8497
8774
|
normalized["payload_type"] = payloadType;
|
|
8498
8775
|
}
|
|
8499
8776
|
normalized.sdk_lang = E2EE_SDK_LANG;
|
|
8500
|
-
delete normalized.
|
|
8777
|
+
delete normalized.sdk_version;
|
|
8501
8778
|
normalized.sdk_version = VERSION;
|
|
8502
8779
|
return normalized;
|
|
8503
8780
|
}
|
|
@@ -9311,19 +9588,18 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
9311
9588
|
public_key: pubDer,
|
|
9312
9589
|
created_at: Date.now()
|
|
9313
9590
|
};
|
|
9314
|
-
await
|
|
9315
|
-
const req = this.store("readwrite").put(record);
|
|
9316
|
-
req.onsuccess = () => resolve();
|
|
9317
|
-
req.onerror = () => reject(req.error);
|
|
9318
|
-
});
|
|
9591
|
+
const aliasKeyId = await spkIdForPubDer(pubDer);
|
|
9319
9592
|
const alias = {
|
|
9320
9593
|
...record,
|
|
9321
|
-
key_id:
|
|
9594
|
+
key_id: aliasKeyId
|
|
9322
9595
|
};
|
|
9323
9596
|
return new Promise((resolve, reject) => {
|
|
9324
|
-
const
|
|
9325
|
-
|
|
9326
|
-
|
|
9597
|
+
const tx = this.db.transaction(V2_STORE_NAME, "readwrite");
|
|
9598
|
+
const store = tx.objectStore(V2_STORE_NAME);
|
|
9599
|
+
store.put(record);
|
|
9600
|
+
store.put(alias);
|
|
9601
|
+
tx.oncomplete = () => resolve();
|
|
9602
|
+
tx.onerror = () => reject(tx.error);
|
|
9327
9603
|
});
|
|
9328
9604
|
}
|
|
9329
9605
|
async loadIK(deviceId) {
|
|
@@ -9411,6 +9687,7 @@ var V2Session = class {
|
|
|
9411
9687
|
__publicField(this, "_oldSPKMaxSeq", /* @__PURE__ */ new Map());
|
|
9412
9688
|
__publicField(this, "_spkCache", /* @__PURE__ */ new Map());
|
|
9413
9689
|
__publicField(this, "_nowFn", () => Date.now());
|
|
9690
|
+
__publicField(this, "_registeringPromise", null);
|
|
9414
9691
|
if (!ikPriv || !ikPubDer) {
|
|
9415
9692
|
throw new Error("V2Session requires AID priv/pub keys (IK = AID identity)");
|
|
9416
9693
|
}
|
|
@@ -9491,9 +9768,21 @@ var V2Session = class {
|
|
|
9491
9768
|
spk_timestamp: spkTimestamp
|
|
9492
9769
|
});
|
|
9493
9770
|
}
|
|
9494
|
-
/** 注册本设备 SPK 到服务端。IK = AID
|
|
9771
|
+
/** 注册本设备 SPK 到服务端。IK = AID 长期密钥,无需注册。幂等,并发安全。 */
|
|
9495
9772
|
async ensureRegistered(callFn) {
|
|
9496
9773
|
if (this._registered) return;
|
|
9774
|
+
if (this._registeringPromise) {
|
|
9775
|
+
await this._registeringPromise;
|
|
9776
|
+
return;
|
|
9777
|
+
}
|
|
9778
|
+
this._registeringPromise = this._doRegister(callFn);
|
|
9779
|
+
try {
|
|
9780
|
+
await this._registeringPromise;
|
|
9781
|
+
} finally {
|
|
9782
|
+
this._registeringPromise = null;
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9785
|
+
async _doRegister(callFn) {
|
|
9497
9786
|
await this.ensureKeys();
|
|
9498
9787
|
const uploadedSPKId = await this._store.loadLatestUploadedSPKId(this._storeDeviceId);
|
|
9499
9788
|
if (uploadedSPKId) {
|
|
@@ -9604,8 +9893,8 @@ var V2Session = class {
|
|
|
9604
9893
|
if (now - info.lastSeenAt < DESTROY_DELAY_MS) continue;
|
|
9605
9894
|
if (recentKeep.has(spkId)) continue;
|
|
9606
9895
|
try {
|
|
9607
|
-
await this._store.deleteSPK(this._deviceId, spkId);
|
|
9608
9896
|
await this._store.deleteSPK(this._storeDeviceId, spkId);
|
|
9897
|
+
await this._store.deleteSPK(this._deviceId, spkId);
|
|
9609
9898
|
} catch (err) {
|
|
9610
9899
|
console.warn("[V2Session] deleteSPK failed", { spkId, err });
|
|
9611
9900
|
continue;
|
|
@@ -9624,7 +9913,8 @@ var V2Session = class {
|
|
|
9624
9913
|
try {
|
|
9625
9914
|
await this._store.deleteSPK(this._storeDeviceId, spkId);
|
|
9626
9915
|
await this._store.deleteSPK(this._deviceId, spkId);
|
|
9627
|
-
} catch {
|
|
9916
|
+
} catch (err) {
|
|
9917
|
+
console.warn("[V2Session] deleteSPK (hard-limit) failed", { spkId, err });
|
|
9628
9918
|
continue;
|
|
9629
9919
|
}
|
|
9630
9920
|
this._oldSPKMaxSeq.delete(spkId);
|
|
@@ -10041,7 +10331,7 @@ var V2E2EECoordinator = class {
|
|
|
10041
10331
|
const gid = String(groupId ?? "").trim();
|
|
10042
10332
|
if (!gid || !client._v2Session) return;
|
|
10043
10333
|
const inflight = this.runtime.v2.groupSpkRegistrationInflight;
|
|
10044
|
-
if (inflight.has(gid)) return;
|
|
10334
|
+
if (inflight.has(gid) || this.runtime.v2.groupSpkRotationInflight.has(gid)) return;
|
|
10045
10335
|
inflight.add(gid);
|
|
10046
10336
|
client._safeAsync((async () => {
|
|
10047
10337
|
try {
|
|
@@ -10059,7 +10349,7 @@ var V2E2EECoordinator = class {
|
|
|
10059
10349
|
const gid = String(groupId ?? "").trim();
|
|
10060
10350
|
if (!gid || !client._v2Session) return;
|
|
10061
10351
|
const inflight = this.runtime.v2.groupSpkRotationInflight;
|
|
10062
|
-
if (inflight.has(gid)) return;
|
|
10352
|
+
if (inflight.has(gid) || this.runtime.v2.groupSpkRegistrationInflight.has(gid)) return;
|
|
10063
10353
|
inflight.add(gid);
|
|
10064
10354
|
client._safeAsync((async () => {
|
|
10065
10355
|
try {
|
|
@@ -10179,6 +10469,13 @@ var V2E2EECoordinator = class {
|
|
|
10179
10469
|
client._v2SenderIKPending.delete(key);
|
|
10180
10470
|
if (plaintext === null) {
|
|
10181
10471
|
client._clientLog.debug(`V2 sender IK pending retry failed: key=${key}`);
|
|
10472
|
+
client.emit("message.undecryptable", {
|
|
10473
|
+
from: entry.fromAid,
|
|
10474
|
+
sender_device_id: entry.senderDeviceId,
|
|
10475
|
+
group_id: entry.groupId || void 0,
|
|
10476
|
+
seq: Number(entry.msg.seq ?? 0),
|
|
10477
|
+
reason: "sender_ik_retry_failed"
|
|
10478
|
+
});
|
|
10182
10479
|
continue;
|
|
10183
10480
|
}
|
|
10184
10481
|
const seq = Number(entry.msg.seq ?? 0);
|
|
@@ -10451,6 +10748,12 @@ var V2E2EECoordinator = class {
|
|
|
10451
10748
|
if (version === "v1") {
|
|
10452
10749
|
const payload = msg.payload;
|
|
10453
10750
|
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
10751
|
+
if (client._delivery.recallEventFromGroupMessage(msg)) {
|
|
10752
|
+
await client._delivery.publishGroupRecallTombstone(gid, seq, msg);
|
|
10753
|
+
client._markPublishedSeq(ns, seq);
|
|
10754
|
+
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
10755
|
+
continue;
|
|
10756
|
+
}
|
|
10454
10757
|
if (payloadObj) {
|
|
10455
10758
|
const payloadType = String(payloadObj.type ?? "").trim();
|
|
10456
10759
|
if (payloadType !== "e2ee.encrypted" && payloadType !== "e2ee.group_encrypted") {
|
|
@@ -11608,11 +11911,11 @@ var GroupStateCoordinator = class {
|
|
|
11608
11911
|
delete data.client_signature;
|
|
11609
11912
|
} else {
|
|
11610
11913
|
const verified = await client._verifyEventSignature(data, cs);
|
|
11611
|
-
if (verified
|
|
11914
|
+
if (!client._isEventSignatureVerified(verified)) {
|
|
11612
11915
|
client._clientLog.warn(`state_committed committer signature verify failed group=%s${String(groupId)}`);
|
|
11613
11916
|
return;
|
|
11614
11917
|
}
|
|
11615
|
-
data._verified =
|
|
11918
|
+
data._verified = true;
|
|
11616
11919
|
}
|
|
11617
11920
|
}
|
|
11618
11921
|
const stateVersion = Number(data.state_version ?? 0);
|
|
@@ -14658,6 +14961,7 @@ var DEFAULT_SESSION_OPTIONS = {
|
|
|
14658
14961
|
var RECONNECT_MIN_BASE_DELAY_SECONDS = 1;
|
|
14659
14962
|
var RECONNECT_MAX_BASE_DELAY_SECONDS = 64;
|
|
14660
14963
|
var TOKEN_REFRESH_CHECK_INTERVAL_MS = 3e4;
|
|
14964
|
+
var MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
|
|
14661
14965
|
var HEARTBEAT_MIN_INTERVAL_SECONDS = 10;
|
|
14662
14966
|
var HEARTBEAT_MAX_INTERVAL_SECONDS = 600;
|
|
14663
14967
|
function clampHeartbeatInterval(value) {
|
|
@@ -14987,6 +15291,8 @@ var _AUNClient = class _AUNClient {
|
|
|
14987
15291
|
__publicField(this, "_pendingOrderedMsgs", /* @__PURE__ */ new Map());
|
|
14988
15292
|
/** Lazy group sync:首次发送群消息前自动拉取历史 */
|
|
14989
15293
|
__publicField(this, "_groupSynced", /* @__PURE__ */ new Set());
|
|
15294
|
+
/** 群撤回去重:group_id|sorted(message_ids)|recalled_at -> 时间戳,保证应用层只回调一次 */
|
|
15295
|
+
__publicField(this, "_groupRecallSeen", /* @__PURE__ */ new Map());
|
|
14990
15296
|
/** 在线未读 hint 队列:同一 group 只保留最后一条,延迟 drain 降低登录瞬时拉取压力。 */
|
|
14991
15297
|
__publicField(this, "_onlineUnreadHintQueue", /* @__PURE__ */ new Map());
|
|
14992
15298
|
__publicField(this, "_onlineUnreadHintTimer", null);
|
|
@@ -15143,6 +15449,9 @@ var _AUNClient = class _AUNClient {
|
|
|
15143
15449
|
this._dispatcher.subscribe("_raw.group.message_created", (data) => {
|
|
15144
15450
|
this._onRawGroupMessageCreated(data);
|
|
15145
15451
|
});
|
|
15452
|
+
this._dispatcher.subscribe("_raw.group.message_recalled", (data) => {
|
|
15453
|
+
this._safeAsync(this._onRawGroupMessageRecalled(data));
|
|
15454
|
+
});
|
|
15146
15455
|
this._dispatcher.subscribe("_raw.group.changed", (data) => {
|
|
15147
15456
|
this._onRawGroupChanged(data);
|
|
15148
15457
|
});
|
|
@@ -15431,6 +15740,85 @@ var _AUNClient = class _AUNClient {
|
|
|
15431
15740
|
async call(method, params) {
|
|
15432
15741
|
return await this._rpcPipeline.call(method, params);
|
|
15433
15742
|
}
|
|
15743
|
+
static _notifyParamsSizeOk(params) {
|
|
15744
|
+
return new TextEncoder().encode(JSON.stringify(params)).length <= MAX_NOTIFY_PAYLOAD_SIZE;
|
|
15745
|
+
}
|
|
15746
|
+
static _validateNotifyEventMethod(method) {
|
|
15747
|
+
const normalized = String(method ?? "").trim();
|
|
15748
|
+
if (!normalized.startsWith("event/app.") || normalized.length <= "event/app.".length) {
|
|
15749
|
+
throw new ValidationError("routed notify method must be event/app.*");
|
|
15750
|
+
}
|
|
15751
|
+
return normalized;
|
|
15752
|
+
}
|
|
15753
|
+
static _normalizeNotifyTtl(value) {
|
|
15754
|
+
if (value === void 0 || value === null) return void 0;
|
|
15755
|
+
const ttl = Number(value);
|
|
15756
|
+
if (!Number.isInteger(ttl)) {
|
|
15757
|
+
throw new ValidationError("ttl_ms must be an integer");
|
|
15758
|
+
}
|
|
15759
|
+
if (ttl < 0 || ttl > 6e4) {
|
|
15760
|
+
throw new ValidationError("ttl_ms must be between 0 and 60000");
|
|
15761
|
+
}
|
|
15762
|
+
return ttl;
|
|
15763
|
+
}
|
|
15764
|
+
/**
|
|
15765
|
+
* 发送轻量在线通知,不走离线存储、seq/pull 或 ack。
|
|
15766
|
+
*/
|
|
15767
|
+
async notify(method, params, options = {}) {
|
|
15768
|
+
if (params !== void 0 && params !== null && !isJsonObject(params)) {
|
|
15769
|
+
throw new ValidationError("notify params must be an object");
|
|
15770
|
+
}
|
|
15771
|
+
const payload = { ...params ?? {} };
|
|
15772
|
+
if (!_AUNClient._notifyParamsSizeOk(payload)) {
|
|
15773
|
+
throw new ValidationError("notify payload is too large");
|
|
15774
|
+
}
|
|
15775
|
+
const targetAid = String(options.to ?? "").trim();
|
|
15776
|
+
const targetGroupId = String(options.group_id ?? options.groupId ?? "").trim();
|
|
15777
|
+
const targetDeviceId = String(options.device_id ?? options.deviceId ?? "").trim();
|
|
15778
|
+
const targetSlotId = String(options.slot_id ?? options.slotId ?? "").trim();
|
|
15779
|
+
const ttl = _AUNClient._normalizeNotifyTtl(options.ttl_ms ?? options.ttlMs);
|
|
15780
|
+
if (targetAid && targetGroupId) {
|
|
15781
|
+
throw new ValidationError("notify() cannot set both to and group_id");
|
|
15782
|
+
}
|
|
15783
|
+
if (targetSlotId && !targetDeviceId) {
|
|
15784
|
+
throw new ValidationError("slot_id requires device_id for notify target");
|
|
15785
|
+
}
|
|
15786
|
+
if (targetAid) {
|
|
15787
|
+
const eventMethod = _AUNClient._validateNotifyEventMethod(method);
|
|
15788
|
+
const target = { type: "aid", aid: targetAid };
|
|
15789
|
+
if (targetDeviceId) target.device_id = targetDeviceId;
|
|
15790
|
+
if (targetSlotId) target.slot_id = targetSlotId;
|
|
15791
|
+
const routeParams = {
|
|
15792
|
+
target,
|
|
15793
|
+
deliver: { method: eventMethod, params: payload }
|
|
15794
|
+
};
|
|
15795
|
+
if (ttl !== void 0) routeParams.ttl_ms = ttl;
|
|
15796
|
+
await this._transport.notify("notification/route", routeParams);
|
|
15797
|
+
return;
|
|
15798
|
+
}
|
|
15799
|
+
if (targetGroupId) {
|
|
15800
|
+
const eventMethod = _AUNClient._validateNotifyEventMethod(method);
|
|
15801
|
+
const normalizedGroupId2 = normalizeGroupId(targetGroupId);
|
|
15802
|
+
if (!normalizedGroupId2) {
|
|
15803
|
+
throw new ValidationError("group_id is required for group notify");
|
|
15804
|
+
}
|
|
15805
|
+
const routeParams = {
|
|
15806
|
+
group_id: normalizedGroupId2,
|
|
15807
|
+
deliver: { method: eventMethod, params: payload }
|
|
15808
|
+
};
|
|
15809
|
+
if (ttl !== void 0) routeParams.ttl_ms = ttl;
|
|
15810
|
+
await this._transport.notify("notification/group.route", routeParams);
|
|
15811
|
+
return;
|
|
15812
|
+
}
|
|
15813
|
+
if (targetDeviceId || targetSlotId) {
|
|
15814
|
+
throw new ValidationError("device_id and slot_id require to");
|
|
15815
|
+
}
|
|
15816
|
+
const directMethod = String(method ?? "").trim();
|
|
15817
|
+
if (!directMethod.startsWith("notification/")) {
|
|
15818
|
+
throw new ValidationError("direct notify method must start with notification/");
|
|
15819
|
+
}
|
|
15820
|
+
await this._transport.notify(directMethod, payload);
|
|
15821
|
+
}
|
|
15434
15822
|
async _callRawV2Rpc(method, params) {
|
|
15435
15823
|
const p = { ...params ?? {} };
|
|
15436
15824
|
delete p._pull_gate_locked;
|
|
@@ -15477,6 +15865,9 @@ var _AUNClient = class _AUNClient {
|
|
|
15477
15865
|
_onRawGroupMessageCreated(data) {
|
|
15478
15866
|
return this._delivery.onRawGroupMessageCreated(data);
|
|
15479
15867
|
}
|
|
15868
|
+
async _onRawGroupMessageRecalled(data) {
|
|
15869
|
+
return this._delivery.onRawGroupMessageRecalled(data);
|
|
15870
|
+
}
|
|
15480
15871
|
/** 处理 V2 群消息通知:主动 pull V2 envelope,由 pullGroupV2 解密并发布。 */
|
|
15481
15872
|
async _onRawGroupV2MessageCreated(data) {
|
|
15482
15873
|
return this._delivery.onRawGroupV2MessageCreated(data);
|
|
@@ -15561,18 +15952,12 @@ var _AUNClient = class _AUNClient {
|
|
|
15561
15952
|
if (this._shouldSkipEventSignature(d)) {
|
|
15562
15953
|
delete d.client_signature;
|
|
15563
15954
|
} else {
|
|
15564
|
-
|
|
15955
|
+
const verified = await this._verifyEventSignature(d, cs);
|
|
15956
|
+
d._verified = this._isEventSignatureVerified(verified);
|
|
15565
15957
|
}
|
|
15566
15958
|
}
|
|
15567
|
-
await this._dispatcher.publish("group.changed", d);
|
|
15568
15959
|
const groupId = d.group_id ?? "";
|
|
15569
|
-
this.
|
|
15570
|
-
this._delivery.handleGroupChangedEventSeq(d, groupId);
|
|
15571
|
-
if (d.action === "dissolved") {
|
|
15572
|
-
if (groupId) {
|
|
15573
|
-
this._cleanupDissolvedGroup(groupId);
|
|
15574
|
-
}
|
|
15575
|
-
}
|
|
15960
|
+
await this._delivery.handleGroupChangedEventSeq(d, groupId);
|
|
15576
15961
|
} else {
|
|
15577
15962
|
await this._dispatcher.publish("group.changed", data);
|
|
15578
15963
|
}
|
|
@@ -15611,6 +15996,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15611
15996
|
this._pushedSeqs.delete(`group:${groupId}`);
|
|
15612
15997
|
this._pushedSeqs.delete(`group_event:${groupId}`);
|
|
15613
15998
|
this._pendingOrderedMsgs.delete(`group:${groupId}`);
|
|
15999
|
+
this._pendingOrderedMsgs.delete(`group_event:${groupId}`);
|
|
15614
16000
|
this._clientLog.info(`cleanup dissolved group ${groupId} local state`);
|
|
15615
16001
|
}
|
|
15616
16002
|
async _verifyEventSignature(_event, cs) {
|
|
@@ -15659,6 +16045,9 @@ var _AUNClient = class _AUNClient {
|
|
|
15659
16045
|
return false;
|
|
15660
16046
|
}
|
|
15661
16047
|
}
|
|
16048
|
+
_isEventSignatureVerified(value) {
|
|
16049
|
+
return value === true;
|
|
16050
|
+
}
|
|
15662
16051
|
_protectedHeadersFromParams(params) {
|
|
15663
16052
|
const value = params.protected_headers ?? params.headers;
|
|
15664
16053
|
if (value == null) return null;
|
|
@@ -16416,7 +16805,7 @@ var _AUNClient = class _AUNClient {
|
|
|
16416
16805
|
);
|
|
16417
16806
|
return repaired;
|
|
16418
16807
|
}
|
|
16419
|
-
async _ensureV2SessionReady(method,
|
|
16808
|
+
async _ensureV2SessionReady(method, errorMessage2) {
|
|
16420
16809
|
if (!this._v2SessionMatchesIdentity()) {
|
|
16421
16810
|
if (!this._v2SessionInitInFlight) {
|
|
16422
16811
|
this._v2SessionInitInFlight = this._initV2Session().finally(() => {
|
|
@@ -16426,7 +16815,7 @@ var _AUNClient = class _AUNClient {
|
|
|
16426
16815
|
await this._v2SessionInitInFlight;
|
|
16427
16816
|
}
|
|
16428
16817
|
if (!this._v2SessionMatchesIdentity()) {
|
|
16429
|
-
throw new StateError(
|
|
16818
|
+
throw new StateError(errorMessage2 ?? `V2 session not initialized; encrypted ${method} requires E2EE V2`);
|
|
16430
16819
|
}
|
|
16431
16820
|
}
|
|
16432
16821
|
_v2CallFn() {
|
|
@@ -18466,6 +18855,1176 @@ var AIDStore = class {
|
|
|
18466
18855
|
// src/index.ts
|
|
18467
18856
|
init_crypto();
|
|
18468
18857
|
|
|
18858
|
+
// src/service-proxy.ts
|
|
18859
|
+
var PROXY_DISCOVERY_CACHE_KEY = "service_proxy_discovery";
|
|
18860
|
+
var PROXY_DISCOVERY_CACHE_TTL_MS = 36e5;
|
|
18861
|
+
var TOKEN_EXPIRY_SKEW_SECONDS = 30;
|
|
18862
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
18863
|
+
"connection",
|
|
18864
|
+
"upgrade",
|
|
18865
|
+
"keep-alive",
|
|
18866
|
+
"proxy-authenticate",
|
|
18867
|
+
"proxy-authorization",
|
|
18868
|
+
"te",
|
|
18869
|
+
"trailer",
|
|
18870
|
+
"transfer-encoding"
|
|
18871
|
+
]);
|
|
18872
|
+
var AUTO_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-length", "date", "server"]);
|
|
18873
|
+
var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "ws:", "wss:"]);
|
|
18874
|
+
var RESERVED_SERVICE_NAMES = /* @__PURE__ */ new Set([
|
|
18875
|
+
"api",
|
|
18876
|
+
"health",
|
|
18877
|
+
"metrics",
|
|
18878
|
+
"status",
|
|
18879
|
+
"proxy",
|
|
18880
|
+
"admin",
|
|
18881
|
+
"ws",
|
|
18882
|
+
"wss",
|
|
18883
|
+
"static",
|
|
18884
|
+
"favicon.ico"
|
|
18885
|
+
]);
|
|
18886
|
+
var SENSITIVE_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
18887
|
+
"endpoint",
|
|
18888
|
+
"url",
|
|
18889
|
+
"uri",
|
|
18890
|
+
"token",
|
|
18891
|
+
"access_token",
|
|
18892
|
+
"authorization",
|
|
18893
|
+
"cookie",
|
|
18894
|
+
"secret",
|
|
18895
|
+
"password",
|
|
18896
|
+
"private_key",
|
|
18897
|
+
"key",
|
|
18898
|
+
"cert",
|
|
18899
|
+
"certificate"
|
|
18900
|
+
]);
|
|
18901
|
+
var SERVICE_NAME_RE = /^[a-z0-9_-]+$/;
|
|
18902
|
+
var STREAMING_SERVICE_TYPES = /* @__PURE__ */ new Set(["mcp", "mcp-sse", "mcp-streamable-http", "sse", "stream", "file", "ws", "websocket"]);
|
|
18903
|
+
var VALID_STREAM_MODES = /* @__PURE__ */ new Set(["auto", "stream", "always", "no_stream"]);
|
|
18904
|
+
var FILE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
18905
|
+
"application/octet-stream",
|
|
18906
|
+
"application/pdf",
|
|
18907
|
+
"application/zip",
|
|
18908
|
+
"application/x-zip-compressed",
|
|
18909
|
+
"application/gzip",
|
|
18910
|
+
"application/x-tar"
|
|
18911
|
+
]);
|
|
18912
|
+
var ServiceRecord = class {
|
|
18913
|
+
constructor(params) {
|
|
18914
|
+
__publicField(this, "service_name");
|
|
18915
|
+
__publicField(this, "endpoint");
|
|
18916
|
+
__publicField(this, "service_type");
|
|
18917
|
+
__publicField(this, "visibility");
|
|
18918
|
+
__publicField(this, "metadata");
|
|
18919
|
+
this.service_name = params.service_name;
|
|
18920
|
+
this.endpoint = params.endpoint;
|
|
18921
|
+
this.service_type = String(params.service_type ?? "http").trim() || "http";
|
|
18922
|
+
this.visibility = String(params.visibility ?? "private").trim() || "private";
|
|
18923
|
+
this.metadata = sanitizeMetadata(params.metadata ?? {});
|
|
18924
|
+
}
|
|
18925
|
+
summary() {
|
|
18926
|
+
return {
|
|
18927
|
+
service_name: this.service_name,
|
|
18928
|
+
service_type: this.service_type,
|
|
18929
|
+
visibility: this.visibility,
|
|
18930
|
+
metadata: sanitizeMetadata(this.metadata)
|
|
18931
|
+
};
|
|
18932
|
+
}
|
|
18933
|
+
};
|
|
18934
|
+
var EndpointPolicy = class {
|
|
18935
|
+
constructor(opts = {}) {
|
|
18936
|
+
__publicField(this, "allowedHosts");
|
|
18937
|
+
this.allowedHosts = new Set(Array.from(opts.allowedHosts ?? []).map(normalizeHost).filter(Boolean));
|
|
18938
|
+
}
|
|
18939
|
+
isAllowed(endpoint) {
|
|
18940
|
+
let parsed;
|
|
18941
|
+
try {
|
|
18942
|
+
parsed = new URL(String(endpoint ?? "").trim());
|
|
18943
|
+
} catch {
|
|
18944
|
+
return false;
|
|
18945
|
+
}
|
|
18946
|
+
if (!ALLOWED_SCHEMES.has(parsed.protocol)) return false;
|
|
18947
|
+
const host = normalizeHost(parsed.hostname);
|
|
18948
|
+
if (!host) return false;
|
|
18949
|
+
if (this.allowedHosts.has(host)) return true;
|
|
18950
|
+
if (host === "localhost") return true;
|
|
18951
|
+
return isIPv4LoopbackHost(host);
|
|
18952
|
+
}
|
|
18953
|
+
};
|
|
18954
|
+
var EmbeddedServiceRegistry = class {
|
|
18955
|
+
constructor(opts = {}) {
|
|
18956
|
+
__publicField(this, "_endpointPolicy");
|
|
18957
|
+
__publicField(this, "_replaceExisting");
|
|
18958
|
+
__publicField(this, "_records", /* @__PURE__ */ new Map());
|
|
18959
|
+
this._endpointPolicy = opts.endpointPolicy ?? new EndpointPolicy();
|
|
18960
|
+
this._replaceExisting = opts.replaceExisting ?? true;
|
|
18961
|
+
}
|
|
18962
|
+
register(serviceName, endpoint, opts = {}) {
|
|
18963
|
+
const normalizedName = normalizeServiceName(serviceName);
|
|
18964
|
+
const endpointText = String(endpoint ?? "").trim();
|
|
18965
|
+
if (!this._endpointPolicy.isAllowed(endpointText)) {
|
|
18966
|
+
throw new ValidationError("endpoint is not allowed");
|
|
18967
|
+
}
|
|
18968
|
+
if (this._records.has(normalizedName) && !this._replaceExisting) {
|
|
18969
|
+
throw new ValidationError(`service already registered: ${normalizedName}`);
|
|
18970
|
+
}
|
|
18971
|
+
const record = new ServiceRecord({
|
|
18972
|
+
service_name: normalizedName,
|
|
18973
|
+
endpoint: endpointText,
|
|
18974
|
+
service_type: opts.serviceType,
|
|
18975
|
+
visibility: opts.visibility,
|
|
18976
|
+
metadata: opts.metadata
|
|
18977
|
+
});
|
|
18978
|
+
this._records.set(normalizedName, record);
|
|
18979
|
+
return record;
|
|
18980
|
+
}
|
|
18981
|
+
unregister(serviceName) {
|
|
18982
|
+
return this._records.delete(normalizeServiceName(serviceName));
|
|
18983
|
+
}
|
|
18984
|
+
get(serviceName) {
|
|
18985
|
+
return this._records.get(normalizeServiceName(serviceName)) ?? null;
|
|
18986
|
+
}
|
|
18987
|
+
listRecords() {
|
|
18988
|
+
return Array.from(this._records.values()).sort((a, b) => a.service_name.localeCompare(b.service_name));
|
|
18989
|
+
}
|
|
18990
|
+
listSummaries() {
|
|
18991
|
+
return this.listRecords().map((record) => record.summary());
|
|
18992
|
+
}
|
|
18993
|
+
};
|
|
18994
|
+
var AsyncQueue = class {
|
|
18995
|
+
constructor() {
|
|
18996
|
+
__publicField(this, "_items", []);
|
|
18997
|
+
__publicField(this, "_waiters", []);
|
|
18998
|
+
__publicField(this, "_closed", false);
|
|
18999
|
+
}
|
|
19000
|
+
push(value) {
|
|
19001
|
+
if (this._closed) return;
|
|
19002
|
+
const waiter = this._waiters.shift();
|
|
19003
|
+
if (waiter) waiter(value);
|
|
19004
|
+
else this._items.push(value);
|
|
19005
|
+
}
|
|
19006
|
+
close() {
|
|
19007
|
+
this._closed = true;
|
|
19008
|
+
for (const waiter of this._waiters.splice(0)) waiter(null);
|
|
19009
|
+
}
|
|
19010
|
+
shift(timeoutMs) {
|
|
19011
|
+
if (this._items.length > 0) return Promise.resolve(this._items.shift());
|
|
19012
|
+
if (this._closed) return Promise.resolve(null);
|
|
19013
|
+
return new Promise((resolve) => {
|
|
19014
|
+
let timer = null;
|
|
19015
|
+
const done = (value) => {
|
|
19016
|
+
if (timer !== null) clearTimeout(timer);
|
|
19017
|
+
resolve(value);
|
|
19018
|
+
};
|
|
19019
|
+
this._waiters.push(done);
|
|
19020
|
+
if (timeoutMs !== void 0) {
|
|
19021
|
+
timer = setTimeout(() => {
|
|
19022
|
+
const idx = this._waiters.indexOf(done);
|
|
19023
|
+
if (idx >= 0) this._waiters.splice(idx, 1);
|
|
19024
|
+
resolve(null);
|
|
19025
|
+
}, Math.max(0, timeoutMs));
|
|
19026
|
+
}
|
|
19027
|
+
});
|
|
19028
|
+
}
|
|
19029
|
+
};
|
|
19030
|
+
var TunnelSocket = class {
|
|
19031
|
+
constructor(ws) {
|
|
19032
|
+
__publicField(this, "_ws");
|
|
19033
|
+
__publicField(this, "_queue", new AsyncQueue());
|
|
19034
|
+
this._ws = ws;
|
|
19035
|
+
try {
|
|
19036
|
+
this._ws.binaryType = "arraybuffer";
|
|
19037
|
+
} catch {
|
|
19038
|
+
}
|
|
19039
|
+
ws.addEventListener("message", (event) => {
|
|
19040
|
+
const data = event.data;
|
|
19041
|
+
if (typeof data === "string") {
|
|
19042
|
+
this._queue.push(data);
|
|
19043
|
+
} else if (data instanceof ArrayBuffer) {
|
|
19044
|
+
this._queue.push(new TextDecoder().decode(new Uint8Array(data)));
|
|
19045
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
19046
|
+
this._queue.push(new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)));
|
|
19047
|
+
} else {
|
|
19048
|
+
this._queue.push(String(data ?? ""));
|
|
19049
|
+
}
|
|
19050
|
+
});
|
|
19051
|
+
ws.addEventListener("close", () => this._queue.close());
|
|
19052
|
+
ws.addEventListener("error", () => this._queue.close());
|
|
19053
|
+
}
|
|
19054
|
+
async send(message) {
|
|
19055
|
+
this._ws.send(JSON.stringify(message));
|
|
19056
|
+
}
|
|
19057
|
+
recv(timeoutMs) {
|
|
19058
|
+
return this._queue.shift(timeoutMs);
|
|
19059
|
+
}
|
|
19060
|
+
close() {
|
|
19061
|
+
try {
|
|
19062
|
+
this._ws.close();
|
|
19063
|
+
} catch {
|
|
19064
|
+
}
|
|
19065
|
+
this._queue.close();
|
|
19066
|
+
}
|
|
19067
|
+
};
|
|
19068
|
+
var ServiceProxyClient = class {
|
|
19069
|
+
constructor(opts) {
|
|
19070
|
+
__publicField(this, "providerAid");
|
|
19071
|
+
__publicField(this, "registry");
|
|
19072
|
+
__publicField(this, "maxResponseBodyBytes");
|
|
19073
|
+
__publicField(this, "maxTunnelMessageBytes");
|
|
19074
|
+
__publicField(this, "_logger");
|
|
19075
|
+
__publicField(this, "_aunClient");
|
|
19076
|
+
__publicField(this, "_webSocketFactory");
|
|
19077
|
+
__publicField(this, "_running", false);
|
|
19078
|
+
__publicField(this, "_activeTunnel", null);
|
|
19079
|
+
this.providerAid = String(opts.providerAid ?? "").trim();
|
|
19080
|
+
this.registry = opts.registry ?? new EmbeddedServiceRegistry({ endpointPolicy: opts.endpointPolicy });
|
|
19081
|
+
this._logger = opts.logger ?? null;
|
|
19082
|
+
this._aunClient = opts.aunClient ?? null;
|
|
19083
|
+
this._webSocketFactory = opts.webSocketFactory ?? null;
|
|
19084
|
+
this.maxResponseBodyBytes = Math.max(1, Math.floor(opts.maxResponseBodyBytes ?? 16 * 1024 * 1024));
|
|
19085
|
+
this.maxTunnelMessageBytes = Math.max(1, Math.floor(opts.maxTunnelMessageBytes ?? 64 * 1024 * 1024));
|
|
19086
|
+
}
|
|
19087
|
+
get isRunning() {
|
|
19088
|
+
return this._running;
|
|
19089
|
+
}
|
|
19090
|
+
get is_running() {
|
|
19091
|
+
return this.isRunning;
|
|
19092
|
+
}
|
|
19093
|
+
stop() {
|
|
19094
|
+
this._running = false;
|
|
19095
|
+
this._activeTunnel?.close();
|
|
19096
|
+
}
|
|
19097
|
+
registerService(serviceName, endpoint, opts = {}) {
|
|
19098
|
+
return this.registry.register(serviceName, endpoint, {
|
|
19099
|
+
serviceType: opts.serviceType ?? opts.service_type,
|
|
19100
|
+
visibility: opts.visibility,
|
|
19101
|
+
metadata: opts.metadata
|
|
19102
|
+
});
|
|
19103
|
+
}
|
|
19104
|
+
register_service(serviceName, endpoint, opts = {}) {
|
|
19105
|
+
return this.registerService(serviceName, endpoint, opts);
|
|
19106
|
+
}
|
|
19107
|
+
unregisterService(serviceName) {
|
|
19108
|
+
return this.registry.unregister(serviceName);
|
|
19109
|
+
}
|
|
19110
|
+
unregister_service(serviceName) {
|
|
19111
|
+
return this.unregisterService(serviceName);
|
|
19112
|
+
}
|
|
19113
|
+
listServiceSummaries() {
|
|
19114
|
+
return this.registry.listSummaries();
|
|
19115
|
+
}
|
|
19116
|
+
list_service_summaries() {
|
|
19117
|
+
return this.listServiceSummaries();
|
|
19118
|
+
}
|
|
19119
|
+
async registerServicesWithGateway(services) {
|
|
19120
|
+
const call = this._gatewayCallMethod(true);
|
|
19121
|
+
const result = await call("proxy.register_services", {
|
|
19122
|
+
provider_aid: this.providerAid,
|
|
19123
|
+
services: services ?? this.listServiceSummaries()
|
|
19124
|
+
});
|
|
19125
|
+
if (!isRecord4(result)) return {};
|
|
19126
|
+
if (result.ok === false) throw new ValidationError(String(result.error ?? "Gateway service registration failed"));
|
|
19127
|
+
return result;
|
|
19128
|
+
}
|
|
19129
|
+
register_services_with_gateway(services) {
|
|
19130
|
+
return this.registerServicesWithGateway(services);
|
|
19131
|
+
}
|
|
19132
|
+
async unregisterServicesFromGateway(serviceNames) {
|
|
19133
|
+
const call = this._gatewayCallMethod(true);
|
|
19134
|
+
const params = { provider_aid: this.providerAid };
|
|
19135
|
+
if (typeof serviceNames === "string") params.service_names = [serviceNames];
|
|
19136
|
+
else if (Array.isArray(serviceNames)) params.service_names = serviceNames.map(String);
|
|
19137
|
+
const result = await call("proxy.unregister_services", params);
|
|
19138
|
+
return isRecord4(result) ? result : {};
|
|
19139
|
+
}
|
|
19140
|
+
unregister_services_from_gateway(serviceNames) {
|
|
19141
|
+
return this.unregisterServicesFromGateway(serviceNames);
|
|
19142
|
+
}
|
|
19143
|
+
async listGatewayServices() {
|
|
19144
|
+
const call = this._gatewayCallMethod(true);
|
|
19145
|
+
const result = await call("proxy.list_services", { provider_aid: this.providerAid });
|
|
19146
|
+
return isRecord4(result) ? result : {};
|
|
19147
|
+
}
|
|
19148
|
+
list_gateway_services() {
|
|
19149
|
+
return this.listGatewayServices();
|
|
19150
|
+
}
|
|
19151
|
+
async discoverProxyServer(opts = {}) {
|
|
19152
|
+
const forceRefresh = Boolean(opts.forceRefresh ?? opts.force_refresh ?? false);
|
|
19153
|
+
if (!forceRefresh) {
|
|
19154
|
+
const cached = await this._loadCachedProxyDiscovery();
|
|
19155
|
+
if (cached) return cached;
|
|
19156
|
+
}
|
|
19157
|
+
const errors = [];
|
|
19158
|
+
for (const url of this._proxyWellKnownUrls()) {
|
|
19159
|
+
try {
|
|
19160
|
+
const discovery = await this._fetchProxyWellKnown(url, opts.timeout ?? 5);
|
|
19161
|
+
await this._persistProxyDiscovery(discovery);
|
|
19162
|
+
return discovery;
|
|
19163
|
+
} catch (exc) {
|
|
19164
|
+
errors.push(`${url}: ${formatError(exc)}`);
|
|
19165
|
+
this._logWarn(`Service Proxy discovery failed: url=${url} err=${formatError(exc)}`);
|
|
19166
|
+
}
|
|
19167
|
+
}
|
|
19168
|
+
throw new ConnectionError(`Service Proxy discovery failed: ${errors.join("; ")}`, { retryable: true });
|
|
19169
|
+
}
|
|
19170
|
+
discover_proxy_server(opts = {}) {
|
|
19171
|
+
return this.discoverProxyServer(opts);
|
|
19172
|
+
}
|
|
19173
|
+
async discoverProxyWsUrl(opts = {}) {
|
|
19174
|
+
const discovery = await this.discoverProxyServer(opts);
|
|
19175
|
+
return String(discovery.ws_url ?? "").trim();
|
|
19176
|
+
}
|
|
19177
|
+
discover_proxy_ws_url(opts = {}) {
|
|
19178
|
+
return this.discoverProxyWsUrl(opts);
|
|
19179
|
+
}
|
|
19180
|
+
async connectOnce(opts = {}) {
|
|
19181
|
+
this._running = true;
|
|
19182
|
+
try {
|
|
19183
|
+
await this._autoRegisterServicesWithGateway();
|
|
19184
|
+
const tunnel = await this._connectProxyWs();
|
|
19185
|
+
this._activeTunnel = tunnel;
|
|
19186
|
+
await tunnel.send({
|
|
19187
|
+
type: "service_proxy_auth",
|
|
19188
|
+
request_id: opts.authRequestId ?? "auth",
|
|
19189
|
+
provider_aid: this.providerAid,
|
|
19190
|
+
client_version: "js"
|
|
19191
|
+
});
|
|
19192
|
+
const authResponse = parseTunnelMessage(await tunnel.recv());
|
|
19193
|
+
if (!authResponse.ok) {
|
|
19194
|
+
const err = isRecord4(authResponse.error) ? authResponse.error : {};
|
|
19195
|
+
throw new AuthError(String(err.message ?? "Service Proxy auth failed"));
|
|
19196
|
+
}
|
|
19197
|
+
const registered = await this.registerServicesWithProxyServer(tunnel, {
|
|
19198
|
+
registerRequestId: opts.registerRequestId ?? "register-services"
|
|
19199
|
+
});
|
|
19200
|
+
let heartbeat = false;
|
|
19201
|
+
if (opts.heartbeatRequestId) {
|
|
19202
|
+
await tunnel.send({ type: "heartbeat", request_id: opts.heartbeatRequestId });
|
|
19203
|
+
heartbeat = Boolean(parseTunnelMessage(await tunnel.recv()).ok);
|
|
19204
|
+
}
|
|
19205
|
+
return { registered, heartbeat };
|
|
19206
|
+
} finally {
|
|
19207
|
+
this._running = false;
|
|
19208
|
+
this._activeTunnel?.close();
|
|
19209
|
+
this._activeTunnel = null;
|
|
19210
|
+
}
|
|
19211
|
+
}
|
|
19212
|
+
connect_once(opts = {}) {
|
|
19213
|
+
return this.connectOnce({
|
|
19214
|
+
authRequestId: opts.auth_request_id,
|
|
19215
|
+
registerRequestId: opts.register_request_id,
|
|
19216
|
+
heartbeatRequestId: opts.heartbeat_request_id
|
|
19217
|
+
});
|
|
19218
|
+
}
|
|
19219
|
+
async serveOnce(opts = {}) {
|
|
19220
|
+
this._running = true;
|
|
19221
|
+
try {
|
|
19222
|
+
await this._autoRegisterServicesWithGateway();
|
|
19223
|
+
const tunnel = await this._connectProxyWs();
|
|
19224
|
+
this._activeTunnel = tunnel;
|
|
19225
|
+
return await this._serveTunnel(tunnel, {
|
|
19226
|
+
authRequestId: opts.authRequestId ?? "auth",
|
|
19227
|
+
registerRequestId: opts.registerRequestId ?? "register-services",
|
|
19228
|
+
maxRequests: opts.maxRequests ?? 1
|
|
19229
|
+
});
|
|
19230
|
+
} finally {
|
|
19231
|
+
this._running = false;
|
|
19232
|
+
this._activeTunnel?.close();
|
|
19233
|
+
this._activeTunnel = null;
|
|
19234
|
+
}
|
|
19235
|
+
}
|
|
19236
|
+
serve_once(opts = {}) {
|
|
19237
|
+
return this.serveOnce({
|
|
19238
|
+
authRequestId: opts.auth_request_id,
|
|
19239
|
+
registerRequestId: opts.register_request_id,
|
|
19240
|
+
maxRequests: opts.max_requests
|
|
19241
|
+
});
|
|
19242
|
+
}
|
|
19243
|
+
async serveForever(opts = {}) {
|
|
19244
|
+
const mode = opts.connectionMode ?? "persistent";
|
|
19245
|
+
if (mode !== "persistent" && mode !== "on_demand") {
|
|
19246
|
+
throw new ValidationError("connectionMode must be persistent or on_demand");
|
|
19247
|
+
}
|
|
19248
|
+
this._running = true;
|
|
19249
|
+
const stats = { connection_mode: mode, connections: 0, registered: 0, handled_requests: 0, wakeup_count: 0 };
|
|
19250
|
+
try {
|
|
19251
|
+
if (mode === "persistent") {
|
|
19252
|
+
while (this._running) {
|
|
19253
|
+
try {
|
|
19254
|
+
await this._autoRegisterServicesWithGateway();
|
|
19255
|
+
const tunnel = await this._connectProxyWs();
|
|
19256
|
+
this._activeTunnel = tunnel;
|
|
19257
|
+
const result = await this._serveTunnel(tunnel, {
|
|
19258
|
+
authRequestId: opts.authRequestId ?? "auth",
|
|
19259
|
+
registerRequestId: opts.registerRequestId ?? "register-services"
|
|
19260
|
+
});
|
|
19261
|
+
stats.connections = Number(stats.connections) + 1;
|
|
19262
|
+
stats.registered = Number(result.registered ?? stats.registered);
|
|
19263
|
+
stats.handled_requests = Number(stats.handled_requests) + Number(result.handled_requests ?? 0);
|
|
19264
|
+
} catch (exc) {
|
|
19265
|
+
if (!this._running) break;
|
|
19266
|
+
this._logWarn(`persistent tunnel reconnect scheduled after error: ${formatError(exc)}`);
|
|
19267
|
+
await sleep(Math.max(0, opts.reconnectDelaySeconds ?? 1) * 1e3);
|
|
19268
|
+
} finally {
|
|
19269
|
+
this._activeTunnel?.close();
|
|
19270
|
+
this._activeTunnel = null;
|
|
19271
|
+
}
|
|
19272
|
+
}
|
|
19273
|
+
return stats;
|
|
19274
|
+
}
|
|
19275
|
+
return await this._serveOnDemand(stats, opts);
|
|
19276
|
+
} finally {
|
|
19277
|
+
this._running = false;
|
|
19278
|
+
this._activeTunnel?.close();
|
|
19279
|
+
this._activeTunnel = null;
|
|
19280
|
+
}
|
|
19281
|
+
}
|
|
19282
|
+
serve_forever(opts = {}) {
|
|
19283
|
+
return this.serveForever({
|
|
19284
|
+
connectionMode: opts.connection_mode,
|
|
19285
|
+
authRequestId: opts.auth_request_id,
|
|
19286
|
+
registerRequestId: opts.register_request_id,
|
|
19287
|
+
idleTimeoutSeconds: opts.idle_timeout_seconds,
|
|
19288
|
+
reconnectDelaySeconds: opts.reconnect_delay_seconds
|
|
19289
|
+
});
|
|
19290
|
+
}
|
|
19291
|
+
async registerServicesWithProxyServer(tunnel, opts = {}) {
|
|
19292
|
+
const services = opts.services ?? this.listServiceSummaries();
|
|
19293
|
+
await tunnel.send({ type: "register_services", request_id: opts.registerRequestId ?? "register-services", services });
|
|
19294
|
+
const response = parseTunnelMessage(await tunnel.recv());
|
|
19295
|
+
if (!response.ok) throw new ValidationError("Service Proxy service registration failed");
|
|
19296
|
+
return Number(response.count ?? services.length);
|
|
19297
|
+
}
|
|
19298
|
+
register_services_with_proxy_server(tunnel, opts = {}) {
|
|
19299
|
+
return this.registerServicesWithProxyServer(tunnel, { registerRequestId: opts.register_request_id, services: opts.services });
|
|
19300
|
+
}
|
|
19301
|
+
async *iterRequestMessages(message, opts = {}) {
|
|
19302
|
+
const requestId = String(message.request_id ?? "");
|
|
19303
|
+
const serviceName = String(message.service_name ?? "");
|
|
19304
|
+
let record = null;
|
|
19305
|
+
try {
|
|
19306
|
+
record = this.registry.get(serviceName);
|
|
19307
|
+
} catch {
|
|
19308
|
+
record = null;
|
|
19309
|
+
}
|
|
19310
|
+
if (!record) {
|
|
19311
|
+
yield errorMessage(requestId, "service_not_registered", "service is not registered");
|
|
19312
|
+
return;
|
|
19313
|
+
}
|
|
19314
|
+
const method = String(message.method ?? "GET").toUpperCase();
|
|
19315
|
+
const path = normalizePath(String(message.path ?? "/"));
|
|
19316
|
+
const targetUrl = buildTargetUrl(record.endpoint, path, String(message.query_string ?? ""));
|
|
19317
|
+
const bodyStream = message.body_stream === true;
|
|
19318
|
+
let body;
|
|
19319
|
+
if (bodyStream) {
|
|
19320
|
+
if (!opts.bodyIter) {
|
|
19321
|
+
yield errorMessage(requestId, "missing_body_stream", "request body stream is missing");
|
|
19322
|
+
return;
|
|
19323
|
+
}
|
|
19324
|
+
body = readableStreamFromAsyncIterable(opts.bodyIter);
|
|
19325
|
+
} else if (message.body_base64) {
|
|
19326
|
+
try {
|
|
19327
|
+
body = toExactArrayBuffer(decodeBase64Strict(String(message.body_base64)));
|
|
19328
|
+
} catch {
|
|
19329
|
+
yield errorMessage(requestId, "invalid_body", "body_base64 is invalid");
|
|
19330
|
+
return;
|
|
19331
|
+
}
|
|
19332
|
+
}
|
|
19333
|
+
const headers = backendHeaders(isRecord4(message.headers) ? message.headers : {});
|
|
19334
|
+
let response;
|
|
19335
|
+
try {
|
|
19336
|
+
const init = { method, headers };
|
|
19337
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
19338
|
+
init.body = body;
|
|
19339
|
+
if (bodyStream) init.duplex = "half";
|
|
19340
|
+
}
|
|
19341
|
+
const controller = new AbortController();
|
|
19342
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
19343
|
+
try {
|
|
19344
|
+
response = await fetch(targetUrl, { ...init, signal: controller.signal });
|
|
19345
|
+
} finally {
|
|
19346
|
+
clearTimeout(timer);
|
|
19347
|
+
}
|
|
19348
|
+
} catch (exc) {
|
|
19349
|
+
this._logWarn(`backend request failed: request_id=${requestId} service_name=${serviceName} err=${formatError(exc)}`);
|
|
19350
|
+
yield errorMessage(requestId, "backend_unreachable", "backend request failed");
|
|
19351
|
+
return;
|
|
19352
|
+
}
|
|
19353
|
+
const responseHeaders = responseHeadersMap(response.headers);
|
|
19354
|
+
const detection = detectRequestProtocol(message, record);
|
|
19355
|
+
const shouldStream = detection.isStream || detection.streamMode !== "no_stream" && isStreamResponseHeaders(responseHeaders);
|
|
19356
|
+
if (!shouldStream) {
|
|
19357
|
+
try {
|
|
19358
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
19359
|
+
if (bytes.length > this.maxResponseBodyBytes) throw new Error("too large");
|
|
19360
|
+
yield {
|
|
19361
|
+
type: "service_proxy_response",
|
|
19362
|
+
request_id: requestId,
|
|
19363
|
+
status: response.status,
|
|
19364
|
+
headers: responseHeaders,
|
|
19365
|
+
body_base64: encodeBase64(bytes)
|
|
19366
|
+
};
|
|
19367
|
+
} catch {
|
|
19368
|
+
yield errorMessage(requestId, "response_body_too_large", "backend response body is too large");
|
|
19369
|
+
}
|
|
19370
|
+
return;
|
|
19371
|
+
}
|
|
19372
|
+
const streamType = streamTypeFromResponse(responseHeaders, detection.serviceType);
|
|
19373
|
+
if (!responseHeaders["x-stream-type"]) responseHeaders["x-stream-type"] = streamType;
|
|
19374
|
+
const chunkSize = Math.max(1, Math.floor(opts.chunkSize ?? 65536));
|
|
19375
|
+
let index = 0;
|
|
19376
|
+
let pending = null;
|
|
19377
|
+
for await (const chunk of responseChunks(response, chunkSize)) {
|
|
19378
|
+
if (pending) {
|
|
19379
|
+
yield streamMessage(requestId, index, response.status, responseHeaders, pending, false);
|
|
19380
|
+
index += 1;
|
|
19381
|
+
}
|
|
19382
|
+
pending = chunk;
|
|
19383
|
+
}
|
|
19384
|
+
if (pending) {
|
|
19385
|
+
yield streamMessage(requestId, index, response.status, responseHeaders, pending, true);
|
|
19386
|
+
} else if (index === 0) {
|
|
19387
|
+
yield { type: "service_proxy_stream", request_id: requestId, index: 0, status: response.status, headers: responseHeaders, data_base64: "", done: true };
|
|
19388
|
+
}
|
|
19389
|
+
}
|
|
19390
|
+
async handleWsConnectMessage(message, tunnel, inboundQueue) {
|
|
19391
|
+
const connectionId = String(message.connection_id ?? "");
|
|
19392
|
+
const serviceName = String(message.service_name ?? "");
|
|
19393
|
+
let record = null;
|
|
19394
|
+
try {
|
|
19395
|
+
record = this.registry.get(serviceName);
|
|
19396
|
+
} catch {
|
|
19397
|
+
record = null;
|
|
19398
|
+
}
|
|
19399
|
+
if (!record) {
|
|
19400
|
+
await tunnel.send(wsErrorMessage(connectionId, "service_not_registered", "service is not registered"));
|
|
19401
|
+
return;
|
|
19402
|
+
}
|
|
19403
|
+
const protocols = Array.isArray(message.subprotocols) ? message.subprotocols.map(String).map((item) => item.trim()).filter(Boolean) : [];
|
|
19404
|
+
let backend;
|
|
19405
|
+
try {
|
|
19406
|
+
backend = this._createWebSocket(
|
|
19407
|
+
buildTargetUrl(record.endpoint, normalizePath(String(message.path ?? "/")), String(message.query_string ?? "")),
|
|
19408
|
+
protocols,
|
|
19409
|
+
{ headers: backendHeaders(isRecord4(message.headers) ? message.headers : {}), verifySsl: this._shouldVerifySsl() },
|
|
19410
|
+
false
|
|
19411
|
+
);
|
|
19412
|
+
await waitForWsOpen(backend);
|
|
19413
|
+
await tunnel.send({ type: "ws_connected", connection_id: connectionId, subprotocol: backend.protocol || "" });
|
|
19414
|
+
} catch (exc) {
|
|
19415
|
+
this._logWarn(`backend websocket bridge failed: connection_id=${connectionId} err=${formatError(exc)}`);
|
|
19416
|
+
await tunnel.send(wsErrorMessage(connectionId, "backend_ws_unreachable", "backend websocket request failed"));
|
|
19417
|
+
return;
|
|
19418
|
+
}
|
|
19419
|
+
backend.binaryType = "arraybuffer";
|
|
19420
|
+
const backendClosed = new Promise((resolve) => {
|
|
19421
|
+
backend.addEventListener("message", (event) => {
|
|
19422
|
+
const data = event.data;
|
|
19423
|
+
if (typeof data === "string") {
|
|
19424
|
+
tunnel.send({ type: "ws_message", connection_id: connectionId, text: data }).catch(() => {
|
|
19425
|
+
});
|
|
19426
|
+
} else {
|
|
19427
|
+
bytesFromWsData(data).then((bytes) => {
|
|
19428
|
+
tunnel.send({ type: "ws_message", connection_id: connectionId, data_base64: encodeBase64(bytes) }).catch(() => {
|
|
19429
|
+
});
|
|
19430
|
+
}).catch(() => {
|
|
19431
|
+
});
|
|
19432
|
+
}
|
|
19433
|
+
});
|
|
19434
|
+
backend.addEventListener("close", (event) => {
|
|
19435
|
+
tunnel.send({ type: "ws_close", connection_id: connectionId, code: event.code || 1e3, reason: "" }).catch(() => {
|
|
19436
|
+
});
|
|
19437
|
+
resolve();
|
|
19438
|
+
});
|
|
19439
|
+
backend.addEventListener("error", () => resolve());
|
|
19440
|
+
});
|
|
19441
|
+
const tunnelToBackend = (async () => {
|
|
19442
|
+
while (this._running) {
|
|
19443
|
+
const item = await inboundQueue.shift();
|
|
19444
|
+
if (!item) return;
|
|
19445
|
+
const msgType = String(item.type ?? "");
|
|
19446
|
+
if (msgType === "ws_message") {
|
|
19447
|
+
if (item.text !== void 0 && item.text !== null) {
|
|
19448
|
+
backend.send(String(item.text));
|
|
19449
|
+
} else if (item.data_base64 !== void 0) {
|
|
19450
|
+
try {
|
|
19451
|
+
backend.send(decodeBase64Strict(String(item.data_base64 ?? "")));
|
|
19452
|
+
} catch {
|
|
19453
|
+
await tunnel.send(wsErrorMessage(connectionId, "invalid_ws_frame", "data_base64 is invalid"));
|
|
19454
|
+
backend.close();
|
|
19455
|
+
return;
|
|
19456
|
+
}
|
|
19457
|
+
}
|
|
19458
|
+
} else if (msgType === "ws_close" || msgType === "ws_error") {
|
|
19459
|
+
backend.close(Number(item.code ?? 1e3), String(item.reason ?? ""));
|
|
19460
|
+
return;
|
|
19461
|
+
}
|
|
19462
|
+
}
|
|
19463
|
+
})();
|
|
19464
|
+
await Promise.race([backendClosed, tunnelToBackend]);
|
|
19465
|
+
try {
|
|
19466
|
+
backend.close();
|
|
19467
|
+
} catch {
|
|
19468
|
+
}
|
|
19469
|
+
}
|
|
19470
|
+
async _serveOnDemand(stats, opts) {
|
|
19471
|
+
const client = this._aunClient;
|
|
19472
|
+
if (!client || typeof client.on !== "function") throw new ValidationError("on_demand mode requires aunClient with on()");
|
|
19473
|
+
await this._autoRegisterServicesWithGateway();
|
|
19474
|
+
const queue = new AsyncQueue();
|
|
19475
|
+
const subscription = client.on("app.service_proxy.wakeup", (payload) => {
|
|
19476
|
+
if (!isRecord4(payload)) return;
|
|
19477
|
+
if (String(payload.type ?? "") !== "aun.service_proxy.wakeup") return;
|
|
19478
|
+
const providerAid = String(payload.provider_aid ?? "").trim();
|
|
19479
|
+
if (providerAid && providerAid !== this.providerAid) return;
|
|
19480
|
+
queue.push({ ...payload });
|
|
19481
|
+
});
|
|
19482
|
+
try {
|
|
19483
|
+
while (this._running) {
|
|
19484
|
+
const wakeup = await queue.shift(100);
|
|
19485
|
+
if (!this._running) break;
|
|
19486
|
+
if (!wakeup) continue;
|
|
19487
|
+
stats.wakeup_count = Number(stats.wakeup_count) + 1;
|
|
19488
|
+
try {
|
|
19489
|
+
await this._autoRegisterServicesWithGateway();
|
|
19490
|
+
const tunnel = await this._connectProxyWs();
|
|
19491
|
+
this._activeTunnel = tunnel;
|
|
19492
|
+
const result = await this._serveTunnel(tunnel, {
|
|
19493
|
+
authRequestId: opts.authRequestId ?? "auth",
|
|
19494
|
+
registerRequestId: opts.registerRequestId ?? "register-services",
|
|
19495
|
+
idleTimeoutSeconds: opts.idleTimeoutSeconds ?? 60
|
|
19496
|
+
});
|
|
19497
|
+
stats.connections = Number(stats.connections) + 1;
|
|
19498
|
+
stats.registered = Number(result.registered ?? stats.registered);
|
|
19499
|
+
stats.handled_requests = Number(stats.handled_requests) + Number(result.handled_requests ?? 0);
|
|
19500
|
+
} catch (exc) {
|
|
19501
|
+
if (!this._running) break;
|
|
19502
|
+
this._logWarn(`on-demand tunnel connection failed after wakeup: ${formatError(exc)}`);
|
|
19503
|
+
await sleep(Math.max(0, opts.reconnectDelaySeconds ?? 1) * 1e3);
|
|
19504
|
+
} finally {
|
|
19505
|
+
this._activeTunnel?.close();
|
|
19506
|
+
this._activeTunnel = null;
|
|
19507
|
+
}
|
|
19508
|
+
}
|
|
19509
|
+
return stats;
|
|
19510
|
+
} finally {
|
|
19511
|
+
subscription?.unsubscribe?.();
|
|
19512
|
+
queue.close();
|
|
19513
|
+
}
|
|
19514
|
+
}
|
|
19515
|
+
async _serveTunnel(tunnel, opts) {
|
|
19516
|
+
let handledRequests = 0;
|
|
19517
|
+
const activeWsQueues = /* @__PURE__ */ new Map();
|
|
19518
|
+
const registered = await this._authAndRegister(tunnel, opts.authRequestId, opts.registerRequestId);
|
|
19519
|
+
try {
|
|
19520
|
+
while (this._running) {
|
|
19521
|
+
if (opts.maxRequests !== void 0 && handledRequests >= opts.maxRequests && activeWsQueues.size === 0) break;
|
|
19522
|
+
const waitForWsTasks = opts.maxRequests !== void 0 && handledRequests >= opts.maxRequests && activeWsQueues.size > 0;
|
|
19523
|
+
const timeoutMs = waitForWsTasks ? 50 : opts.idleTimeoutSeconds === void 0 ? void 0 : opts.idleTimeoutSeconds * 1e3;
|
|
19524
|
+
const raw = await tunnel.recv(timeoutMs);
|
|
19525
|
+
if (raw === null) {
|
|
19526
|
+
if (timeoutMs !== void 0 && activeWsQueues.size > 0) continue;
|
|
19527
|
+
break;
|
|
19528
|
+
}
|
|
19529
|
+
let message;
|
|
19530
|
+
try {
|
|
19531
|
+
const parsed = JSON.parse(raw);
|
|
19532
|
+
if (!isRecord4(parsed)) continue;
|
|
19533
|
+
message = parsed;
|
|
19534
|
+
} catch {
|
|
19535
|
+
continue;
|
|
19536
|
+
}
|
|
19537
|
+
const msgType = String(message.type ?? "");
|
|
19538
|
+
if (msgType === "service_proxy_request") {
|
|
19539
|
+
const requestId = String(message.request_id ?? "");
|
|
19540
|
+
const bodyIter = message.body_stream === true ? this._iterRequestBodyChunks(tunnel, requestId, activeWsQueues) : void 0;
|
|
19541
|
+
for await (const response of this.iterRequestMessages(message, { bodyIter })) await tunnel.send(response);
|
|
19542
|
+
handledRequests += 1;
|
|
19543
|
+
} else if (msgType === "ws_connect") {
|
|
19544
|
+
const connectionId = String(message.connection_id ?? "");
|
|
19545
|
+
if (!connectionId) {
|
|
19546
|
+
await tunnel.send(wsErrorMessage("", "missing_connection_id", "connection_id is required"));
|
|
19547
|
+
continue;
|
|
19548
|
+
}
|
|
19549
|
+
const queue = new AsyncQueue();
|
|
19550
|
+
activeWsQueues.set(connectionId, queue);
|
|
19551
|
+
this.handleWsConnectMessage(message, tunnel, queue).finally(() => {
|
|
19552
|
+
queue.close();
|
|
19553
|
+
activeWsQueues.delete(connectionId);
|
|
19554
|
+
});
|
|
19555
|
+
handledRequests += 1;
|
|
19556
|
+
} else if (msgType === "ws_message" || msgType === "ws_close" || msgType === "ws_error") {
|
|
19557
|
+
const connectionId = String(message.connection_id ?? "");
|
|
19558
|
+
const queue = activeWsQueues.get(connectionId);
|
|
19559
|
+
if (queue) queue.push(message);
|
|
19560
|
+
else if (connectionId) await tunnel.send(wsErrorMessage(connectionId, "unknown_ws_connection", "WebSocket connection is not active"));
|
|
19561
|
+
} else if (msgType !== "heartbeat_ack") {
|
|
19562
|
+
await tunnel.send(errorMessage(String(message.request_id ?? ""), "unsupported_message", "unsupported Service Proxy tunnel message"));
|
|
19563
|
+
}
|
|
19564
|
+
}
|
|
19565
|
+
return { registered, handled_requests: handledRequests };
|
|
19566
|
+
} finally {
|
|
19567
|
+
for (const queue of activeWsQueues.values()) queue.close();
|
|
19568
|
+
}
|
|
19569
|
+
}
|
|
19570
|
+
async _authAndRegister(tunnel, authRequestId, registerRequestId) {
|
|
19571
|
+
await tunnel.send({ type: "service_proxy_auth", request_id: authRequestId, provider_aid: this.providerAid, client_version: "js" });
|
|
19572
|
+
const authResponse = parseTunnelMessage(await tunnel.recv());
|
|
19573
|
+
if (!authResponse.ok) {
|
|
19574
|
+
const err = isRecord4(authResponse.error) ? authResponse.error : {};
|
|
19575
|
+
throw new AuthError(String(err.message ?? "Service Proxy auth failed"));
|
|
19576
|
+
}
|
|
19577
|
+
return this.registerServicesWithProxyServer(tunnel, { registerRequestId });
|
|
19578
|
+
}
|
|
19579
|
+
async *_iterRequestBodyChunks(tunnel, requestId, activeWsQueues) {
|
|
19580
|
+
while (true) {
|
|
19581
|
+
const message = parseTunnelMessage(await tunnel.recv());
|
|
19582
|
+
const msgType = String(message.type ?? "");
|
|
19583
|
+
if (msgType === "ws_message" || msgType === "ws_close" || msgType === "ws_error") {
|
|
19584
|
+
const queue = activeWsQueues.get(String(message.connection_id ?? ""));
|
|
19585
|
+
if (queue) {
|
|
19586
|
+
queue.push(message);
|
|
19587
|
+
continue;
|
|
19588
|
+
}
|
|
19589
|
+
}
|
|
19590
|
+
if (msgType !== "service_proxy_request_body") throw new Error("invalid_body_stream");
|
|
19591
|
+
if (String(message.request_id ?? "") !== requestId) throw new Error("request body stream request_id mismatch");
|
|
19592
|
+
if (isRecord4(message.error)) throw new Error(String(message.error.message ?? "request body stream failed"));
|
|
19593
|
+
const dataText = String(message.data_base64 ?? "");
|
|
19594
|
+
if (dataText) yield decodeBase64Strict(dataText);
|
|
19595
|
+
if (message.done === true) return;
|
|
19596
|
+
}
|
|
19597
|
+
}
|
|
19598
|
+
_createWebSocket(url, protocols, options, requireHeaders) {
|
|
19599
|
+
if (this._webSocketFactory) return this._webSocketFactory(url, protocols, options);
|
|
19600
|
+
if (requireHeaders && options.headers && Object.keys(options.headers).length > 0) {
|
|
19601
|
+
throw new AuthError("Browser WebSocket cannot set Authorization header; pass webSocketFactory to ServiceProxyClient");
|
|
19602
|
+
}
|
|
19603
|
+
return new WebSocket(url, protocols);
|
|
19604
|
+
}
|
|
19605
|
+
async _connectProxyWs() {
|
|
19606
|
+
const proxyUrl = await this.discoverProxyWsUrl();
|
|
19607
|
+
const token = await this._ensureAccessToken();
|
|
19608
|
+
if (!token) throw new AuthError("AUN access_token is required for Service Proxy tunnel");
|
|
19609
|
+
const ws = this._createWebSocket(
|
|
19610
|
+
proxyUrl,
|
|
19611
|
+
void 0,
|
|
19612
|
+
{
|
|
19613
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
19614
|
+
maxPayloadBytes: this.maxTunnelMessageBytes,
|
|
19615
|
+
verifySsl: this._shouldVerifySsl()
|
|
19616
|
+
},
|
|
19617
|
+
true
|
|
19618
|
+
);
|
|
19619
|
+
await waitForWsOpen(ws);
|
|
19620
|
+
return new TunnelSocket(ws);
|
|
19621
|
+
}
|
|
19622
|
+
_gatewayCallMethod(required) {
|
|
19623
|
+
const call = this._aunClient?.call;
|
|
19624
|
+
if (typeof call === "function") return (method, params) => Promise.resolve(call.call(this._aunClient, method, params ?? {}));
|
|
19625
|
+
if (required) throw new ValidationError("Gateway service registration requires aunClient with call()");
|
|
19626
|
+
return async () => ({ skipped: true });
|
|
19627
|
+
}
|
|
19628
|
+
async _autoRegisterServicesWithGateway() {
|
|
19629
|
+
const call = this._aunClient?.call;
|
|
19630
|
+
if (typeof call !== "function") return { skipped: true };
|
|
19631
|
+
return this.registerServicesWithGateway();
|
|
19632
|
+
}
|
|
19633
|
+
_issuerDomainForAid(aid) {
|
|
19634
|
+
const target = String(aid ?? "").trim().toLowerCase();
|
|
19635
|
+
if (!target.includes(".")) return "";
|
|
19636
|
+
return target.split(".").slice(1).join(".").replace(/^\.+|\.+$/g, "");
|
|
19637
|
+
}
|
|
19638
|
+
_proxyWellKnownUrls() {
|
|
19639
|
+
const issuer = this._issuerDomainForAid(this.providerAid);
|
|
19640
|
+
if (!this.providerAid || !issuer) throw new ValidationError("providerAid must be a full AID for Service Proxy discovery");
|
|
19641
|
+
return [`https://${this.providerAid}/.well-known/aun-proxy`, `https://proxy.${issuer}/.well-known/aun-proxy`];
|
|
19642
|
+
}
|
|
19643
|
+
_normalizeProxyWsUrl(rawUrl) {
|
|
19644
|
+
const value = String(rawUrl ?? "").trim();
|
|
19645
|
+
if (!value) return "";
|
|
19646
|
+
let parsed;
|
|
19647
|
+
try {
|
|
19648
|
+
parsed = new URL(value);
|
|
19649
|
+
} catch {
|
|
19650
|
+
return "";
|
|
19651
|
+
}
|
|
19652
|
+
if (parsed.protocol === "ws:" && this._shouldVerifySsl()) return "";
|
|
19653
|
+
if (parsed.protocol !== "wss:" && parsed.protocol !== "ws:") return "";
|
|
19654
|
+
if (parsed.username || parsed.password || !parsed.hostname || parsed.pathname === "/") return "";
|
|
19655
|
+
parsed.hash = "";
|
|
19656
|
+
return parsed.toString();
|
|
19657
|
+
}
|
|
19658
|
+
_selectProxyWsUrl(payload) {
|
|
19659
|
+
const direct = this._normalizeProxyWsUrl(String(payload.ws_url ?? ""));
|
|
19660
|
+
if (direct) return direct;
|
|
19661
|
+
const servers = Array.isArray(payload.proxy_servers) ? payload.proxy_servers.filter(isRecord4) : [];
|
|
19662
|
+
servers.sort((a, b) => Number(a.priority ?? 999) - Number(b.priority ?? 999));
|
|
19663
|
+
for (const item of servers) {
|
|
19664
|
+
const url = this._normalizeProxyWsUrl(String(item.ws_url ?? ""));
|
|
19665
|
+
if (url) return url;
|
|
19666
|
+
}
|
|
19667
|
+
return "";
|
|
19668
|
+
}
|
|
19669
|
+
async _fetchProxyWellKnown(wellKnownUrl, timeoutSeconds) {
|
|
19670
|
+
const controller = new AbortController();
|
|
19671
|
+
const timer = setTimeout(() => controller.abort(), Math.max(100, timeoutSeconds * 1e3));
|
|
19672
|
+
try {
|
|
19673
|
+
const response = await fetch(wellKnownUrl, { signal: controller.signal });
|
|
19674
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
19675
|
+
const payload = await response.json();
|
|
19676
|
+
if (!isRecord4(payload)) throw new ValidationError("Service Proxy well-known returned invalid payload");
|
|
19677
|
+
const wsUrl = this._selectProxyWsUrl(payload);
|
|
19678
|
+
if (!wsUrl) throw new ValidationError("Service Proxy well-known missing valid ws_url");
|
|
19679
|
+
return { ...payload, ws_url: wsUrl, source_url: wellKnownUrl, discovered_at: Date.now() / 1e3 };
|
|
19680
|
+
} finally {
|
|
19681
|
+
clearTimeout(timer);
|
|
19682
|
+
}
|
|
19683
|
+
}
|
|
19684
|
+
async _loadCachedProxyDiscovery() {
|
|
19685
|
+
const tokenStore = this._aunClient?._tokenStore;
|
|
19686
|
+
if (!tokenStore) return null;
|
|
19687
|
+
try {
|
|
19688
|
+
let raw = "";
|
|
19689
|
+
if (typeof tokenStore.getMetadata === "function") raw = await tokenStore.getMetadata(this.providerAid, PROXY_DISCOVERY_CACHE_KEY);
|
|
19690
|
+
else if (typeof tokenStore.loadMetadata === "function") raw = (await tokenStore.loadMetadata(this.providerAid))?.[PROXY_DISCOVERY_CACHE_KEY];
|
|
19691
|
+
const cached = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
19692
|
+
if (!isRecord4(cached)) return null;
|
|
19693
|
+
const wsUrl = this._normalizeProxyWsUrl(String(cached.ws_url ?? ""));
|
|
19694
|
+
if (!wsUrl) return null;
|
|
19695
|
+
const discoveredAt = Number(cached.discovered_at ?? 0);
|
|
19696
|
+
if (!Number.isFinite(discoveredAt) || Date.now() - discoveredAt * 1e3 >= PROXY_DISCOVERY_CACHE_TTL_MS) return null;
|
|
19697
|
+
return { ...cached, ws_url: wsUrl, cached: true };
|
|
19698
|
+
} catch {
|
|
19699
|
+
return null;
|
|
19700
|
+
}
|
|
19701
|
+
}
|
|
19702
|
+
async _persistProxyDiscovery(discovery) {
|
|
19703
|
+
const tokenStore = this._aunClient?._tokenStore;
|
|
19704
|
+
if (!tokenStore || typeof tokenStore.setMetadata !== "function" || !this.providerAid) return;
|
|
19705
|
+
try {
|
|
19706
|
+
await tokenStore.setMetadata(this.providerAid, PROXY_DISCOVERY_CACHE_KEY, JSON.stringify(discovery));
|
|
19707
|
+
} catch (exc) {
|
|
19708
|
+
this._logWarn(`Service Proxy discovery cache write failed: ${formatError(exc)}`);
|
|
19709
|
+
}
|
|
19710
|
+
}
|
|
19711
|
+
_shouldVerifySsl() {
|
|
19712
|
+
const client = this._aunClient;
|
|
19713
|
+
const cfg = client?.configModel ?? client?._configModel;
|
|
19714
|
+
if (cfg && (typeof cfg.verifySsl === "boolean" || typeof cfg.verify_ssl === "boolean")) return Boolean(cfg.verifySsl ?? cfg.verify_ssl);
|
|
19715
|
+
const aid = client?.currentAid ?? client?._currentAid;
|
|
19716
|
+
if (aid && (typeof aid.verifySsl === "boolean" || typeof aid.verify_ssl === "boolean")) return Boolean(aid.verifySsl ?? aid.verify_ssl);
|
|
19717
|
+
return true;
|
|
19718
|
+
}
|
|
19719
|
+
_mappingAccessToken(mapping) {
|
|
19720
|
+
if (!mapping) return "";
|
|
19721
|
+
const token = String(mapping.access_token ?? mapping.token ?? mapping.kite_token ?? "").trim();
|
|
19722
|
+
if (!token) return "";
|
|
19723
|
+
const expiresAt = Number(mapping.access_token_expires_at ?? mapping.expires_at ?? 0);
|
|
19724
|
+
if (Number.isFinite(expiresAt) && expiresAt > 0 && expiresAt <= Date.now() / 1e3 + TOKEN_EXPIRY_SKEW_SECONDS) return "";
|
|
19725
|
+
return token;
|
|
19726
|
+
}
|
|
19727
|
+
async _resolveCachedAccessToken() {
|
|
19728
|
+
const client = this._aunClient;
|
|
19729
|
+
if (!client) return "";
|
|
19730
|
+
const direct = this._mappingAccessToken(client);
|
|
19731
|
+
if (direct) return direct;
|
|
19732
|
+
if (isRecord4(client._identity)) {
|
|
19733
|
+
const token = this._mappingAccessToken(client._identity);
|
|
19734
|
+
if (token) return token;
|
|
19735
|
+
}
|
|
19736
|
+
const auth = client._auth;
|
|
19737
|
+
if (auth && typeof auth.loadIdentityOrNone === "function") {
|
|
19738
|
+
try {
|
|
19739
|
+
const token = this._mappingAccessToken(await auth.loadIdentityOrNone(this.providerAid));
|
|
19740
|
+
if (token) return token;
|
|
19741
|
+
} catch {
|
|
19742
|
+
}
|
|
19743
|
+
}
|
|
19744
|
+
const tokenStore = client._tokenStore;
|
|
19745
|
+
if (tokenStore && typeof tokenStore.loadInstanceState === "function") {
|
|
19746
|
+
try {
|
|
19747
|
+
const deviceId = String(client.deviceId ?? client.device_id ?? client._deviceId ?? client._device_id ?? "");
|
|
19748
|
+
const slotId = String(client.slotId ?? client.slot_id ?? client._slotId ?? client._slot_id ?? "");
|
|
19749
|
+
const token = this._mappingAccessToken(await tokenStore.loadInstanceState(this.providerAid, deviceId, slotId));
|
|
19750
|
+
if (token) return token;
|
|
19751
|
+
} catch {
|
|
19752
|
+
}
|
|
19753
|
+
}
|
|
19754
|
+
return "";
|
|
19755
|
+
}
|
|
19756
|
+
async _authenticateForAccessToken() {
|
|
19757
|
+
const authenticate = this._aunClient?.authenticate;
|
|
19758
|
+
if (typeof authenticate !== "function") throw new AuthError("Service Proxy tunnel requires aunClient.authenticate() for AUN token authentication");
|
|
19759
|
+
let result;
|
|
19760
|
+
try {
|
|
19761
|
+
result = await authenticate.call(this._aunClient);
|
|
19762
|
+
} catch (exc) {
|
|
19763
|
+
throw new AuthError(`AUNClient authenticate failed for Service Proxy tunnel: ${formatError(exc)}`);
|
|
19764
|
+
}
|
|
19765
|
+
const token = this._mappingAccessToken(isRecord4(result) ? result : null);
|
|
19766
|
+
if (token) return token;
|
|
19767
|
+
throw new AuthError("AUNClient authenticate did not return a valid access_token");
|
|
19768
|
+
}
|
|
19769
|
+
async _ensureAccessToken() {
|
|
19770
|
+
return await this._resolveCachedAccessToken() || await this._authenticateForAccessToken();
|
|
19771
|
+
}
|
|
19772
|
+
_logWarn(message) {
|
|
19773
|
+
try {
|
|
19774
|
+
this._logger?.warn(message);
|
|
19775
|
+
} catch {
|
|
19776
|
+
}
|
|
19777
|
+
}
|
|
19778
|
+
};
|
|
19779
|
+
function normalizeServiceName(serviceName) {
|
|
19780
|
+
const value = String(serviceName ?? "").trim();
|
|
19781
|
+
if (!value) throw new ValidationError("service_name is required");
|
|
19782
|
+
if (RESERVED_SERVICE_NAMES.has(value)) throw new ValidationError("service_name is reserved");
|
|
19783
|
+
if (!SERVICE_NAME_RE.test(value)) throw new ValidationError("service_name must match [a-z0-9_-]+");
|
|
19784
|
+
return value;
|
|
19785
|
+
}
|
|
19786
|
+
function normalizeHost(host) {
|
|
19787
|
+
return String(host ?? "").trim().toLowerCase().replace(/\.+$/g, "");
|
|
19788
|
+
}
|
|
19789
|
+
function isIPv4LoopbackHost(host) {
|
|
19790
|
+
const parts = host.split(".");
|
|
19791
|
+
if (parts.length !== 4 || parts[0] !== "127") return false;
|
|
19792
|
+
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255);
|
|
19793
|
+
}
|
|
19794
|
+
function isRecord4(value) {
|
|
19795
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19796
|
+
}
|
|
19797
|
+
function isSensitiveMetadataKey(key) {
|
|
19798
|
+
const normalized = key.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
19799
|
+
return SENSITIVE_METADATA_KEYS.has(normalized) || /(_token|_secret|_password|_private_key)$/.test(normalized);
|
|
19800
|
+
}
|
|
19801
|
+
function sanitizeMetadata(metadata) {
|
|
19802
|
+
const out = {};
|
|
19803
|
+
for (const [key, value] of Object.entries(metadata ?? {})) {
|
|
19804
|
+
if (isSensitiveMetadataKey(key)) continue;
|
|
19805
|
+
if (isRecord4(value)) out[key] = sanitizeMetadata(value);
|
|
19806
|
+
else if (Array.isArray(value)) out[key] = value.map((item) => isRecord4(item) ? sanitizeMetadata(item) : item);
|
|
19807
|
+
else out[key] = value;
|
|
19808
|
+
}
|
|
19809
|
+
return out;
|
|
19810
|
+
}
|
|
19811
|
+
function headersMap(headers) {
|
|
19812
|
+
const result = {};
|
|
19813
|
+
if (!isRecord4(headers)) return result;
|
|
19814
|
+
for (const [key, value] of Object.entries(headers)) result[key.toLowerCase()] = String(value);
|
|
19815
|
+
return result;
|
|
19816
|
+
}
|
|
19817
|
+
function streamModeFrom(headers, record, message) {
|
|
19818
|
+
let value = String(message.stream_mode ?? "").trim().toLowerCase();
|
|
19819
|
+
if (!value) value = String(headers["x-stream-mode"] ?? "").trim().toLowerCase();
|
|
19820
|
+
if (!value) value = String(record.metadata.stream_mode ?? "").trim().toLowerCase();
|
|
19821
|
+
if (value === "always") return "stream";
|
|
19822
|
+
return VALID_STREAM_MODES.has(value) ? value : "auto";
|
|
19823
|
+
}
|
|
19824
|
+
function detectRequestProtocol(message, record) {
|
|
19825
|
+
const headers = headersMap(isRecord4(message.headers) ? message.headers : {});
|
|
19826
|
+
const streamMode = streamModeFrom(headers, record, message);
|
|
19827
|
+
let serviceType = String(message.service_type ?? "").trim().toLowerCase() || record.service_type.toLowerCase() || "http";
|
|
19828
|
+
if (streamMode === "no_stream") {
|
|
19829
|
+
serviceType = "http";
|
|
19830
|
+
} else if (!message.service_type) {
|
|
19831
|
+
const explicitType = String(headers["x-service-type"] ?? "").trim().toLowerCase();
|
|
19832
|
+
const method = String(message.method ?? "").toUpperCase();
|
|
19833
|
+
const path = String(message.path ?? "").toLowerCase();
|
|
19834
|
+
const accept = String(headers.accept ?? "").toLowerCase();
|
|
19835
|
+
const contentType = String(headers["content-type"] ?? "").toLowerCase();
|
|
19836
|
+
if (explicitType) serviceType = explicitType;
|
|
19837
|
+
else if (accept.includes("text/event-stream")) serviceType = "sse";
|
|
19838
|
+
else if ("mcp-session-id" in headers) serviceType = "mcp";
|
|
19839
|
+
else if (method === "POST" && bodyHasJsonRpc(message)) serviceType = "mcp";
|
|
19840
|
+
else if (contentType.startsWith("application/grpc")) serviceType = "ws";
|
|
19841
|
+
else if (path.includes("/mcp")) serviceType = "mcp";
|
|
19842
|
+
else if (path.includes("/sse") || path.includes("/events")) serviceType = "sse";
|
|
19843
|
+
else if (path.includes("/download") || path.includes("/files/")) serviceType = "file";
|
|
19844
|
+
}
|
|
19845
|
+
let isStream;
|
|
19846
|
+
if (streamMode === "stream") isStream = true;
|
|
19847
|
+
else if (streamMode === "no_stream") isStream = false;
|
|
19848
|
+
else if ("is_stream" in message) isStream = Boolean(message.is_stream);
|
|
19849
|
+
else if ("stream" in message) isStream = Boolean(message.stream);
|
|
19850
|
+
else isStream = STREAMING_SERVICE_TYPES.has(serviceType);
|
|
19851
|
+
return { serviceType, streamMode, isStream };
|
|
19852
|
+
}
|
|
19853
|
+
function bodyHasJsonRpc(message) {
|
|
19854
|
+
const raw = String(message.body_base64 ?? "");
|
|
19855
|
+
if (!raw) return false;
|
|
19856
|
+
let text = "";
|
|
19857
|
+
try {
|
|
19858
|
+
text = new TextDecoder().decode(decodeBase64Strict(raw));
|
|
19859
|
+
} catch {
|
|
19860
|
+
return false;
|
|
19861
|
+
}
|
|
19862
|
+
if (text.includes('"jsonrpc"') || text.includes("'jsonrpc'")) return true;
|
|
19863
|
+
try {
|
|
19864
|
+
const parsed = JSON.parse(text);
|
|
19865
|
+
if (isRecord4(parsed)) return String(parsed.jsonrpc ?? "") === "2.0";
|
|
19866
|
+
if (Array.isArray(parsed)) return parsed.some((item) => isRecord4(item) && String(item.jsonrpc ?? "") === "2.0");
|
|
19867
|
+
} catch {
|
|
19868
|
+
}
|
|
19869
|
+
return false;
|
|
19870
|
+
}
|
|
19871
|
+
function backendHeaders(headers) {
|
|
19872
|
+
const result = {};
|
|
19873
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
19874
|
+
const name = key.toLowerCase();
|
|
19875
|
+
if (HOP_BY_HOP_HEADERS.has(name) || name === "host") continue;
|
|
19876
|
+
result[name] = String(value);
|
|
19877
|
+
}
|
|
19878
|
+
return result;
|
|
19879
|
+
}
|
|
19880
|
+
function responseHeadersMap(headers) {
|
|
19881
|
+
const result = {};
|
|
19882
|
+
headers.forEach((value, key) => {
|
|
19883
|
+
const name = key.toLowerCase();
|
|
19884
|
+
if (HOP_BY_HOP_HEADERS.has(name) || AUTO_RESPONSE_HEADERS.has(name)) return;
|
|
19885
|
+
result[name] = value;
|
|
19886
|
+
});
|
|
19887
|
+
return result;
|
|
19888
|
+
}
|
|
19889
|
+
function isStreamResponseHeaders(headers) {
|
|
19890
|
+
const contentType = String(headers["content-type"] ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
19891
|
+
const contentDisposition = String(headers["content-disposition"] ?? "").toLowerCase();
|
|
19892
|
+
if (String(headers["content-type"] ?? "").toLowerCase().includes("text/event-stream")) return true;
|
|
19893
|
+
if (FILE_CONTENT_TYPES.has(contentType)) return true;
|
|
19894
|
+
if (contentType.startsWith("image/") || contentType.startsWith("video/")) return true;
|
|
19895
|
+
return contentDisposition.includes("attachment");
|
|
19896
|
+
}
|
|
19897
|
+
function streamTypeFromResponse(headers, fallback) {
|
|
19898
|
+
const contentType = String(headers["content-type"] ?? "").toLowerCase();
|
|
19899
|
+
if (contentType.includes("text/event-stream")) return "sse";
|
|
19900
|
+
if (isStreamResponseHeaders(headers)) return "file";
|
|
19901
|
+
return String(fallback || "stream").trim().toLowerCase() || "stream";
|
|
19902
|
+
}
|
|
19903
|
+
function normalizePath(path) {
|
|
19904
|
+
const text = String(path || "/");
|
|
19905
|
+
return text.startsWith("/") ? text : `/${text}`;
|
|
19906
|
+
}
|
|
19907
|
+
function buildTargetUrl(endpoint, path, queryString) {
|
|
19908
|
+
const base = endpoint.replace(/\/+$/g, "") + "/";
|
|
19909
|
+
const url = new URL(path.replace(/^\/+/g, ""), base);
|
|
19910
|
+
if (queryString) url.search = queryString.startsWith("?") ? queryString : `?${queryString}`;
|
|
19911
|
+
return url.toString();
|
|
19912
|
+
}
|
|
19913
|
+
function errorMessage(requestId, code, message) {
|
|
19914
|
+
return { type: "service_proxy_error", request_id: requestId, error: { code, message } };
|
|
19915
|
+
}
|
|
19916
|
+
function wsErrorMessage(connectionId, code, message) {
|
|
19917
|
+
return { type: "ws_error", connection_id: connectionId, error: { code, message } };
|
|
19918
|
+
}
|
|
19919
|
+
function streamMessage(requestId, index, status, headers, data, done) {
|
|
19920
|
+
return { type: "service_proxy_stream", request_id: requestId, index, status: index === 0 ? status : null, headers: index === 0 ? headers : {}, data_base64: encodeBase64(data), done };
|
|
19921
|
+
}
|
|
19922
|
+
function parseTunnelMessage(raw) {
|
|
19923
|
+
if (raw === null) throw new ConnectionError("Service Proxy tunnel closed");
|
|
19924
|
+
const parsed = JSON.parse(raw);
|
|
19925
|
+
return isRecord4(parsed) ? parsed : {};
|
|
19926
|
+
}
|
|
19927
|
+
function waitForWsOpen(ws) {
|
|
19928
|
+
return new Promise((resolve, reject) => {
|
|
19929
|
+
const cleanup = () => {
|
|
19930
|
+
ws.removeEventListener("open", onOpen);
|
|
19931
|
+
ws.removeEventListener("error", onError);
|
|
19932
|
+
};
|
|
19933
|
+
const onOpen = (_event) => {
|
|
19934
|
+
cleanup();
|
|
19935
|
+
resolve();
|
|
19936
|
+
};
|
|
19937
|
+
const onError = (_event) => {
|
|
19938
|
+
cleanup();
|
|
19939
|
+
reject(new ConnectionError("websocket connect failed"));
|
|
19940
|
+
};
|
|
19941
|
+
ws.addEventListener("open", onOpen);
|
|
19942
|
+
ws.addEventListener("error", onError);
|
|
19943
|
+
});
|
|
19944
|
+
}
|
|
19945
|
+
async function* responseChunks(response, chunkSize) {
|
|
19946
|
+
if (!response.body) {
|
|
19947
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
19948
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) yield bytes.slice(offset, offset + chunkSize);
|
|
19949
|
+
return;
|
|
19950
|
+
}
|
|
19951
|
+
const reader = response.body.getReader();
|
|
19952
|
+
try {
|
|
19953
|
+
while (true) {
|
|
19954
|
+
const { done, value } = await reader.read();
|
|
19955
|
+
if (done) return;
|
|
19956
|
+
if (!value) continue;
|
|
19957
|
+
for (let offset = 0; offset < value.length; offset += chunkSize) yield value.slice(offset, offset + chunkSize);
|
|
19958
|
+
}
|
|
19959
|
+
} finally {
|
|
19960
|
+
reader.releaseLock();
|
|
19961
|
+
}
|
|
19962
|
+
}
|
|
19963
|
+
function readableStreamFromAsyncIterable(iterable) {
|
|
19964
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
19965
|
+
return new ReadableStream({
|
|
19966
|
+
async pull(controller) {
|
|
19967
|
+
const { done, value } = await iterator.next();
|
|
19968
|
+
if (done) controller.close();
|
|
19969
|
+
else controller.enqueue(value);
|
|
19970
|
+
},
|
|
19971
|
+
async cancel() {
|
|
19972
|
+
await iterator.return?.();
|
|
19973
|
+
}
|
|
19974
|
+
});
|
|
19975
|
+
}
|
|
19976
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
19977
|
+
function encodeBase64(bytes) {
|
|
19978
|
+
let out = "";
|
|
19979
|
+
let i = 0;
|
|
19980
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
19981
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
19982
|
+
out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + B64[n >> 6 & 63] + B64[n & 63];
|
|
19983
|
+
}
|
|
19984
|
+
if (i < bytes.length) {
|
|
19985
|
+
const a = bytes[i];
|
|
19986
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
19987
|
+
const n = a << 16 | b << 8;
|
|
19988
|
+
out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + (i + 1 < bytes.length ? B64[n >> 6 & 63] : "=") + "=";
|
|
19989
|
+
}
|
|
19990
|
+
return out;
|
|
19991
|
+
}
|
|
19992
|
+
function decodeBase64Strict(value) {
|
|
19993
|
+
const text = String(value ?? "").trim();
|
|
19994
|
+
if (!text) return new Uint8Array();
|
|
19995
|
+
if (text.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) throw new Error("invalid base64");
|
|
19996
|
+
const clean2 = text.replace(/=+$/g, "");
|
|
19997
|
+
const bytes = [];
|
|
19998
|
+
let buffer = 0;
|
|
19999
|
+
let bits = 0;
|
|
20000
|
+
for (const ch of clean2) {
|
|
20001
|
+
const v = B64.indexOf(ch);
|
|
20002
|
+
if (v < 0) throw new Error("invalid base64");
|
|
20003
|
+
buffer = buffer << 6 | v;
|
|
20004
|
+
bits += 6;
|
|
20005
|
+
if (bits >= 8) {
|
|
20006
|
+
bits -= 8;
|
|
20007
|
+
bytes.push(buffer >> bits & 255);
|
|
20008
|
+
}
|
|
20009
|
+
}
|
|
20010
|
+
return new Uint8Array(bytes);
|
|
20011
|
+
}
|
|
20012
|
+
function toExactArrayBuffer(bytes) {
|
|
20013
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
20014
|
+
}
|
|
20015
|
+
async function bytesFromWsData(data) {
|
|
20016
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
20017
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
20018
|
+
if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
20019
|
+
return new TextEncoder().encode(String(data ?? ""));
|
|
20020
|
+
}
|
|
20021
|
+
function sleep(ms) {
|
|
20022
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20023
|
+
}
|
|
20024
|
+
function formatError(error) {
|
|
20025
|
+
return error instanceof Error ? error.message : String(error);
|
|
20026
|
+
}
|
|
20027
|
+
|
|
18469
20028
|
// src/secret-store/index.ts
|
|
18470
20029
|
async function createDefaultSecretStore(encryptionSeed) {
|
|
18471
20030
|
const { IndexedDBSecretStore: IndexedDBSecretStore2 } = await Promise.resolve().then(() => (init_indexeddb_store(), indexeddb_store_exports));
|
|
@@ -18538,6 +20097,8 @@ export {
|
|
|
18538
20097
|
E2EEGroupEpochMismatchError,
|
|
18539
20098
|
E2EEGroupNotMemberError,
|
|
18540
20099
|
E2EEGroupSecretMissingError,
|
|
20100
|
+
EmbeddedServiceRegistry,
|
|
20101
|
+
EndpointPolicy,
|
|
18541
20102
|
EventDispatcher,
|
|
18542
20103
|
GatewayDiscovery,
|
|
18543
20104
|
GroupError,
|
|
@@ -18557,6 +20118,8 @@ export {
|
|
|
18557
20118
|
STATE_PREFIX,
|
|
18558
20119
|
SeedMigrationError,
|
|
18559
20120
|
SerializationError,
|
|
20121
|
+
ServiceProxyClient,
|
|
20122
|
+
ServiceRecord,
|
|
18560
20123
|
SessionError,
|
|
18561
20124
|
StateError,
|
|
18562
20125
|
Subscription,
|