@joe-broadhead/messaging-bin-win32-x64 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/messagingd.exe +2 -2
- package/lib/messagingd.mjs +1398 -1299
- package/package.json +1 -1
package/lib/messagingd.mjs
CHANGED
|
@@ -5129,12 +5129,6 @@ function _ensureDefaultMemoizer() {
|
|
|
5129
5129
|
function string2(params) {
|
|
5130
5130
|
return _string(ZodString, params);
|
|
5131
5131
|
}
|
|
5132
|
-
function ipv42(params) {
|
|
5133
|
-
return _ipv4(ZodIPv4, params);
|
|
5134
|
-
}
|
|
5135
|
-
function ipv62(params) {
|
|
5136
|
-
return _ipv6(ZodIPv6, params);
|
|
5137
|
-
}
|
|
5138
5132
|
function number2(params) {
|
|
5139
5133
|
return _number(ZodNumber, params);
|
|
5140
5134
|
}
|
|
@@ -6122,12 +6116,11 @@ var init_common = __esm(() => {
|
|
|
6122
6116
|
card: `messaging/1/card
|
|
6123
6117
|
`,
|
|
6124
6118
|
seal: "messaging/1/seal",
|
|
6125
|
-
directAuth: `messaging/1/direct-auth
|
|
6126
|
-
`,
|
|
6127
|
-
directLink: "messaging/1/direct-link",
|
|
6128
6119
|
person: `messaging/1/person
|
|
6129
6120
|
`,
|
|
6130
6121
|
tombstone: "messaging/1/tombstone",
|
|
6122
|
+
dirRead: `messaging/1/dir-read
|
|
6123
|
+
`,
|
|
6131
6124
|
deviceLog: `messaging/1/device-log
|
|
6132
6125
|
`,
|
|
6133
6126
|
deviceKeys: `messaging/1/device-keys
|
|
@@ -6163,7 +6156,7 @@ var init_common = __esm(() => {
|
|
|
6163
6156
|
Millis = number2().int().nonnegative();
|
|
6164
6157
|
Address = string2().min(1).max(200);
|
|
6165
6158
|
SessionAddress = string2().regex(/^(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})\/[a-z0-9][a-z0-9._-]{0,63}@[a-z0-9][a-z0-9-]{0,31}$/, "person/session@label");
|
|
6166
|
-
MemberRole = _enum(["admin", "member"
|
|
6159
|
+
MemberRole = _enum(["admin", "member"]);
|
|
6167
6160
|
SessionState = _enum(["idle", "busy"]);
|
|
6168
6161
|
DeliveryTier = _enum(["push", "pull"]);
|
|
6169
6162
|
DeliveryStage = _enum([
|
|
@@ -6404,7 +6397,7 @@ var init_jelly = __esm(() => {
|
|
|
6404
6397
|
});
|
|
6405
6398
|
|
|
6406
6399
|
// packages/core/dist/schema/directory.js
|
|
6407
|
-
var MapProof, MapValue, DeviceId, DeviceKind, UnbindReason, Signature, LinkRequest, Authority, LogEntry, KeysStatement, EpochLeaf, EpochProof, STH, KeyRotation, EntryEpoch, PersonView;
|
|
6400
|
+
var MapProof, MapValue, DeviceId, DeviceKind, UnbindReason, Signature, LinkRequest, Authority, LogEntry, KeysStatement, EpochLeaf, EpochProof, STH, KeyRotation, EntryEpoch, PersonHead, PersonView;
|
|
6408
6401
|
var init_directory = __esm(() => {
|
|
6409
6402
|
init_zod();
|
|
6410
6403
|
init_common();
|
|
@@ -6499,6 +6492,12 @@ var init_directory = __esm(() => {
|
|
|
6499
6492
|
value: MapValue,
|
|
6500
6493
|
mapProof: MapProof
|
|
6501
6494
|
});
|
|
6495
|
+
PersonHead = strictObject({
|
|
6496
|
+
person: HostedPersonId,
|
|
6497
|
+
mapProof: MapProof,
|
|
6498
|
+
value: MapValue.optional(),
|
|
6499
|
+
tombstone: literal(true).optional()
|
|
6500
|
+
});
|
|
6502
6501
|
PersonView = strictObject({
|
|
6503
6502
|
person: HostedPersonId,
|
|
6504
6503
|
log: array(LogEntry).max(1024),
|
|
@@ -6510,7 +6509,7 @@ var init_directory = __esm(() => {
|
|
|
6510
6509
|
});
|
|
6511
6510
|
|
|
6512
6511
|
// packages/core/dist/schema/a2a.js
|
|
6513
|
-
var Struct, TaskState, TERMINAL_STATES, Role, Part, Message, TaskStatus, Artifact, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent
|
|
6512
|
+
var Struct, TaskState, TERMINAL_STATES, Role, Part, Message, TaskStatus, Artifact, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent;
|
|
6514
6513
|
var init_a2a = __esm(() => {
|
|
6515
6514
|
init_zod();
|
|
6516
6515
|
Struct = record(string2(), unknown());
|
|
@@ -6587,92 +6586,6 @@ var init_a2a = __esm(() => {
|
|
|
6587
6586
|
lastChunk: boolean2().optional(),
|
|
6588
6587
|
metadata: Struct.optional()
|
|
6589
6588
|
});
|
|
6590
|
-
AgentInterface = object({
|
|
6591
|
-
url: string2().min(1),
|
|
6592
|
-
protocolBinding: string2().min(1).describe("JSONRPC | GRPC | HTTP+JSON | a binding URI"),
|
|
6593
|
-
tenant: string2().optional(),
|
|
6594
|
-
protocolVersion: string2().min(1)
|
|
6595
|
-
});
|
|
6596
|
-
AgentExtension = object({
|
|
6597
|
-
uri: string2(),
|
|
6598
|
-
description: string2().optional(),
|
|
6599
|
-
required: boolean2().optional(),
|
|
6600
|
-
params: Struct.optional()
|
|
6601
|
-
});
|
|
6602
|
-
AgentCapabilities = object({
|
|
6603
|
-
streaming: boolean2().optional(),
|
|
6604
|
-
pushNotifications: boolean2().optional(),
|
|
6605
|
-
extensions: array(AgentExtension).optional(),
|
|
6606
|
-
extendedAgentCard: boolean2().optional()
|
|
6607
|
-
});
|
|
6608
|
-
AgentSkill = object({
|
|
6609
|
-
id: string2().min(1),
|
|
6610
|
-
name: string2().min(1),
|
|
6611
|
-
description: string2(),
|
|
6612
|
-
tags: array(string2()),
|
|
6613
|
-
examples: array(string2()).optional(),
|
|
6614
|
-
inputModes: array(string2()).optional(),
|
|
6615
|
-
outputModes: array(string2()).optional(),
|
|
6616
|
-
securityRequirements: array(unknown()).optional()
|
|
6617
|
-
});
|
|
6618
|
-
AgentCard = object({
|
|
6619
|
-
name: string2().min(1),
|
|
6620
|
-
description: string2(),
|
|
6621
|
-
supportedInterfaces: array(AgentInterface).min(1),
|
|
6622
|
-
provider: object({ url: string2(), organization: string2() }).optional(),
|
|
6623
|
-
version: string2(),
|
|
6624
|
-
documentationUrl: string2().optional(),
|
|
6625
|
-
capabilities: AgentCapabilities,
|
|
6626
|
-
securitySchemes: record(string2(), unknown()).optional(),
|
|
6627
|
-
securityRequirements: array(unknown()).optional(),
|
|
6628
|
-
defaultInputModes: array(string2()),
|
|
6629
|
-
defaultOutputModes: array(string2()),
|
|
6630
|
-
skills: array(AgentSkill),
|
|
6631
|
-
signatures: array(object({ protected: string2(), signature: string2(), header: Struct.optional() })).optional(),
|
|
6632
|
-
iconUrl: string2().optional()
|
|
6633
|
-
});
|
|
6634
|
-
SendMessageConfiguration = object({
|
|
6635
|
-
acceptedOutputModes: array(string2()).optional(),
|
|
6636
|
-
taskPushNotificationConfig: unknown().optional(),
|
|
6637
|
-
historyLength: number2().int().nonnegative().optional(),
|
|
6638
|
-
returnImmediately: boolean2().optional()
|
|
6639
|
-
});
|
|
6640
|
-
SendMessageRequest = object({
|
|
6641
|
-
tenant: string2().optional(),
|
|
6642
|
-
message: Message,
|
|
6643
|
-
configuration: SendMessageConfiguration.optional(),
|
|
6644
|
-
metadata: Struct.optional()
|
|
6645
|
-
});
|
|
6646
|
-
SendMessageResponse = union([object({ task: Task }), object({ message: Message })]);
|
|
6647
|
-
StreamResponse = union([
|
|
6648
|
-
object({ task: Task }),
|
|
6649
|
-
object({ message: Message }),
|
|
6650
|
-
object({ statusUpdate: TaskStatusUpdateEvent }),
|
|
6651
|
-
object({ artifactUpdate: TaskArtifactUpdateEvent })
|
|
6652
|
-
]);
|
|
6653
|
-
GetTaskRequest = object({
|
|
6654
|
-
tenant: string2().optional(),
|
|
6655
|
-
id: string2().min(1),
|
|
6656
|
-
historyLength: number2().int().nonnegative().optional()
|
|
6657
|
-
});
|
|
6658
|
-
CancelTaskRequest = object({ tenant: string2().optional(), id: string2().min(1), metadata: Struct.optional() });
|
|
6659
|
-
SubscribeToTaskRequest = object({ tenant: string2().optional(), id: string2().min(1) });
|
|
6660
|
-
ListTasksRequest = object({
|
|
6661
|
-
tenant: string2().optional(),
|
|
6662
|
-
contextId: string2().optional(),
|
|
6663
|
-
status: TaskState.or(literal("TASK_STATE_UNSPECIFIED")).optional(),
|
|
6664
|
-
pageSize: number2().int().min(1).max(100).optional(),
|
|
6665
|
-
pageToken: string2().optional(),
|
|
6666
|
-
historyLength: number2().int().nonnegative().optional(),
|
|
6667
|
-
statusTimestampAfter: string2().optional(),
|
|
6668
|
-
includeArtifacts: boolean2().optional()
|
|
6669
|
-
});
|
|
6670
|
-
ListTasksResponse = object({
|
|
6671
|
-
tasks: array(Task),
|
|
6672
|
-
nextPageToken: string2(),
|
|
6673
|
-
pageSize: number2().int(),
|
|
6674
|
-
totalSize: number2().int()
|
|
6675
|
-
});
|
|
6676
6589
|
});
|
|
6677
6590
|
|
|
6678
6591
|
// packages/core/dist/schema/consent.js
|
|
@@ -6760,13 +6673,12 @@ var init_consent = __esm(() => {
|
|
|
6760
6673
|
});
|
|
6761
6674
|
|
|
6762
6675
|
// packages/core/dist/schema/delegation.js
|
|
6763
|
-
var DelegationScope,
|
|
6676
|
+
var DelegationScope, DelegationGrant, DelegationChain, DelegationSummary;
|
|
6764
6677
|
var init_delegation = __esm(() => {
|
|
6765
6678
|
init_zod();
|
|
6766
6679
|
init_common();
|
|
6767
6680
|
init_realm();
|
|
6768
6681
|
DelegationScope = string2().regex(/^[a-z0-9][a-z0-9._:/-]{0,63}$/, "scope label");
|
|
6769
|
-
ExternalPrincipal = string2().min(1).max(64);
|
|
6770
6682
|
DelegationGrant = looseObject({
|
|
6771
6683
|
realm: RealmId,
|
|
6772
6684
|
task: Ulid,
|
|
@@ -6778,7 +6690,6 @@ var init_delegation = __esm(() => {
|
|
|
6778
6690
|
maxDepth: number2().int().min(1).max(LIMITS.delegationDepth),
|
|
6779
6691
|
scope: array(DelegationScope).max(LIMITS.delegationScopes).optional(),
|
|
6780
6692
|
parent: Hash.optional(),
|
|
6781
|
-
external: looseObject({ principal: ExternalPrincipal }).optional(),
|
|
6782
6693
|
sig: Base64Url
|
|
6783
6694
|
});
|
|
6784
6695
|
DelegationChain = array(DelegationGrant).min(1).max(LIMITS.delegationDepth);
|
|
@@ -6787,7 +6698,6 @@ var init_delegation = __esm(() => {
|
|
|
6787
6698
|
depth: number2().int().min(1).max(LIMITS.delegationDepth),
|
|
6788
6699
|
maxDepth: number2().int().min(1).max(LIMITS.delegationDepth),
|
|
6789
6700
|
scope: array(DelegationScope).max(LIMITS.delegationScopes).optional(),
|
|
6790
|
-
external: object({ principal: ExternalPrincipal, gateway: MachineId }).optional(),
|
|
6791
6701
|
exp: Millis
|
|
6792
6702
|
});
|
|
6793
6703
|
});
|
|
@@ -6800,7 +6710,7 @@ function requestMessage(message) {
|
|
|
6800
6710
|
function cardMessage(message, what) {
|
|
6801
6711
|
return bare(message, "data") && (what === "card" ? CardData : CardGetData).safeParse(message.parts[0].data).success;
|
|
6802
6712
|
}
|
|
6803
|
-
var EnvelopeType, SEAL_ALG = "x25519-hkdf-sha256-a256gcm", EnvelopeClass, Party, Sender, Recipient, Envelope, ChannelPersons, ChannelConv, SelfConv, RequestConv, CardConv, Conv, MessagePayload, BARE_MESSAGE, bare = (message, part) => Object.keys(message).every((key) => BARE_MESSAGE.has(key)) && message.parts.length === 1 && Object.keys(message.parts[0]).join() === part, TaskStatusPayload, TaskArtifactPayload, TaskCancelPayload, ReceiptPayload, SessionInfo,
|
|
6713
|
+
var EnvelopeType, SEAL_ALG = "x25519-hkdf-sha256-a256gcm", EnvelopeClass, Party, Sender, Recipient, Envelope, ChannelPersons, ChannelConv, SelfConv, RequestConv, CardConv, Conv, MessagePayload, BARE_MESSAGE, bare = (message, part) => Object.keys(message).every((key) => BARE_MESSAGE.has(key)) && message.parts.length === 1 && Object.keys(message.parts[0]).join() === part, TaskStatusPayload, TaskArtifactPayload, TaskCancelPayload, ReceiptPayload, SessionInfo, PresencePayload, PAYLOAD_BY_TYPE, Provenance;
|
|
6804
6714
|
var init_envelope = __esm(() => {
|
|
6805
6715
|
init_zod();
|
|
6806
6716
|
init_a2a();
|
|
@@ -6855,7 +6765,6 @@ var init_envelope = __esm(() => {
|
|
|
6855
6765
|
kind: _enum(["message", "task"]),
|
|
6856
6766
|
message: Message,
|
|
6857
6767
|
wake: boolean2().optional(),
|
|
6858
|
-
external: object({ principal: string2().min(1).max(64) }).optional(),
|
|
6859
6768
|
delegation: DelegationChain.optional(),
|
|
6860
6769
|
conv: Conv.optional()
|
|
6861
6770
|
}).refine((p) => p.kind === "task" ? p.message.taskId !== undefined : true, { message: "a task message needs message.taskId" }).refine((p) => p.kind === "task" === (p.delegation !== undefined), { message: "a task request carries a delegation chain, and only a task request" }).refine((p) => p.conv?.kind !== "self" || p.message.parts.length === 1 && GrantSync.safeParse(p.message.parts[0].data).success, {
|
|
@@ -6892,16 +6801,11 @@ var init_envelope = __esm(() => {
|
|
|
6892
6801
|
kept: literal(true).optional(),
|
|
6893
6802
|
wake: literal(true).optional()
|
|
6894
6803
|
});
|
|
6895
|
-
DirectEndpoint = object({
|
|
6896
|
-
host: union([ipv42(), ipv62()]),
|
|
6897
|
-
port: number2().int().min(1).max(65535)
|
|
6898
|
-
});
|
|
6899
6804
|
PresencePayload = object({
|
|
6900
6805
|
seq: number2().int().nonnegative(),
|
|
6901
6806
|
label: MachineLabel,
|
|
6902
6807
|
sessions: array(SessionInfo).max(LIMITS.presenceSessions),
|
|
6903
6808
|
features: array(string2().max(64)).max(32).optional(),
|
|
6904
|
-
direct: array(DirectEndpoint).min(1).max(4).optional(),
|
|
6905
6809
|
away: literal(true).optional()
|
|
6906
6810
|
});
|
|
6907
6811
|
PAYLOAD_BY_TYPE = {
|
|
@@ -6920,8 +6824,7 @@ var init_envelope = __esm(() => {
|
|
|
6920
6824
|
device: MachineId,
|
|
6921
6825
|
label: MachineLabel,
|
|
6922
6826
|
verified: literal("signature"),
|
|
6923
|
-
receivedAt: Millis
|
|
6924
|
-
external: object({ principal: string2().max(64), gateway: MachineId }).optional()
|
|
6827
|
+
receivedAt: Millis
|
|
6925
6828
|
});
|
|
6926
6829
|
});
|
|
6927
6830
|
|
|
@@ -6974,7 +6877,6 @@ var init_roster = __esm(() => {
|
|
|
6974
6877
|
inviteId: Ulid,
|
|
6975
6878
|
invitePub: Key32,
|
|
6976
6879
|
person: PersonName,
|
|
6977
|
-
role: _enum(["member", "service"]),
|
|
6978
6880
|
expires: Millis
|
|
6979
6881
|
})
|
|
6980
6882
|
});
|
|
@@ -7011,7 +6913,6 @@ var init_roster = __esm(() => {
|
|
|
7011
6913
|
inviteId: Ulid,
|
|
7012
6914
|
secret: Key32,
|
|
7013
6915
|
person: PersonName,
|
|
7014
|
-
role: _enum(["member", "service"]),
|
|
7015
6916
|
expires: Millis
|
|
7016
6917
|
});
|
|
7017
6918
|
});
|
|
@@ -7096,6 +6997,7 @@ var init_relay = __esm(() => {
|
|
|
7096
6997
|
protocolError: 4002,
|
|
7097
6998
|
removed: 4003,
|
|
7098
6999
|
replaced: 4004,
|
|
7000
|
+
full: 1013,
|
|
7099
7001
|
tooLarge: 1009
|
|
7100
7002
|
};
|
|
7101
7003
|
});
|
|
@@ -7104,7 +7006,10 @@ var init_relay = __esm(() => {
|
|
|
7104
7006
|
function hostedRelayAuthPayload(args) {
|
|
7105
7007
|
return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, person: args.person, device: args.device, nonce: args.nonce, ts: args.ts };
|
|
7106
7008
|
}
|
|
7107
|
-
|
|
7009
|
+
function dirReadPayload(args) {
|
|
7010
|
+
return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, device: args.device, nonce: args.nonce };
|
|
7011
|
+
}
|
|
7012
|
+
var Req2, Size, Handle, Email, OrgId, DirReadProof, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirHeadsFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, WAKE_AGENTS = 64, WAKE_AWAITING = 500, WAKE_TICKET_BYTES = 4096, WAKE_TICKET_MAX_S, WakeUrl, WakeArmFrame, WakeDisarmFrame, DirectoryClientFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, DirHeadsAreFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
|
|
7108
7013
|
var init_directory_frames = __esm(() => {
|
|
7109
7014
|
init_zod();
|
|
7110
7015
|
init_common();
|
|
@@ -7117,6 +7022,7 @@ var init_directory_frames = __esm(() => {
|
|
|
7117
7022
|
Handle = string2().regex(/^[a-z][a-z0-9-]{0,31}$/, "lowercase handle");
|
|
7118
7023
|
Email = string2().max(254).regex(/^[^\s@A-Z]{1,64}@(?:[a-z0-9-]+\.)+[a-z0-9-]+$/, "lower-case email address");
|
|
7119
7024
|
OrgId = string2().min(1).max(200);
|
|
7025
|
+
DirReadProof = strictObject({ device: DeviceId, sig: Base64Url });
|
|
7120
7026
|
DirBindFrame = object({ t: literal("dir.bind"), req: Req2, entry: LogEntry });
|
|
7121
7027
|
HostedHelloFrame = object({
|
|
7122
7028
|
t: literal("hello"),
|
|
@@ -7139,6 +7045,13 @@ var init_directory_frames = __esm(() => {
|
|
|
7139
7045
|
t: literal("dir.lookup"),
|
|
7140
7046
|
req: Req2,
|
|
7141
7047
|
persons: array(object({ person: HostedPersonId, knownSeq: number2().int().nonnegative().optional() })).min(1).max(LIMITS.dirLookupPersons),
|
|
7048
|
+
size: Size.optional(),
|
|
7049
|
+
proof: DirReadProof.optional()
|
|
7050
|
+
});
|
|
7051
|
+
DirHeadsFrame = object({
|
|
7052
|
+
t: literal("dir.heads"),
|
|
7053
|
+
req: Req2,
|
|
7054
|
+
persons: array(HostedPersonId).min(1).max(LIMITS.dirLookupPersons),
|
|
7142
7055
|
size: Size.optional()
|
|
7143
7056
|
});
|
|
7144
7057
|
DirDeviceFrame = object({
|
|
@@ -7188,6 +7101,7 @@ var init_directory_frames = __esm(() => {
|
|
|
7188
7101
|
DirSthFrame,
|
|
7189
7102
|
DirLookupFrame,
|
|
7190
7103
|
DirDeviceFrame,
|
|
7104
|
+
DirHeadsFrame,
|
|
7191
7105
|
DirResolveFrame,
|
|
7192
7106
|
DirSearchFrame,
|
|
7193
7107
|
DirWatchFrame,
|
|
@@ -7220,6 +7134,13 @@ var init_directory_frames = __esm(() => {
|
|
|
7220
7134
|
head: EpochProof,
|
|
7221
7135
|
persons: array(PersonView).max(LIMITS.dirLookupPersons)
|
|
7222
7136
|
});
|
|
7137
|
+
DirHeadsAreFrame = object({
|
|
7138
|
+
t: literal("dir.headsAre"),
|
|
7139
|
+
req: Req2,
|
|
7140
|
+
sth: STH,
|
|
7141
|
+
head: EpochProof,
|
|
7142
|
+
persons: array(PersonHead).max(LIMITS.dirLookupPersons)
|
|
7143
|
+
});
|
|
7223
7144
|
ResolvedPerson = object({
|
|
7224
7145
|
person: HostedPersonId,
|
|
7225
7146
|
handle: Handle,
|
|
@@ -7243,47 +7164,22 @@ var init_directory_frames = __esm(() => {
|
|
|
7243
7164
|
DirOkFrame,
|
|
7244
7165
|
DirSthIsFrame,
|
|
7245
7166
|
DirPersonsFrame,
|
|
7167
|
+
DirHeadsAreFrame,
|
|
7246
7168
|
DirResolvedFrame,
|
|
7247
7169
|
DirChangedFrame,
|
|
7248
7170
|
DirUnboundFrame
|
|
7249
7171
|
]);
|
|
7250
7172
|
});
|
|
7251
7173
|
|
|
7252
|
-
// packages/core/dist/schema/direct.js
|
|
7253
|
-
var DIRECT_PROTOCOL = "messaging-direct/1", DIRECT_HANDSHAKE_BYTES = 4096, DirectHalf, DirectHelloFrame, DirectAcceptFrame, DirectAuthFrame, DirectIdentity, DirectSendRecord, DirectAckRecord, DirectNackRecord, DirectRecord;
|
|
7254
|
-
var init_direct = __esm(() => {
|
|
7255
|
-
init_zod();
|
|
7256
|
-
init_common();
|
|
7257
|
-
init_errors3();
|
|
7258
|
-
DirectHalf = object({ epk: Key32, nonce: Key32 });
|
|
7259
|
-
DirectHelloFrame = object({ t: literal("hello"), proto: literal(DIRECT_PROTOCOL), epk: Key32, nonce: Key32 });
|
|
7260
|
-
DirectAcceptFrame = object({ t: literal("accept"), epk: Key32, nonce: Key32, box: Base64Url.max(1024) });
|
|
7261
|
-
DirectAuthFrame = object({ t: literal("auth"), box: Base64Url.max(1024) });
|
|
7262
|
-
DirectIdentity = object({ team: TeamId, machine: MachineId, sig: Base64Url });
|
|
7263
|
-
DirectSendRecord = object({
|
|
7264
|
-
t: literal("send"),
|
|
7265
|
-
env: looseObject({
|
|
7266
|
-
v: string2(),
|
|
7267
|
-
id: Ulid,
|
|
7268
|
-
type: string2(),
|
|
7269
|
-
from: looseObject({ device: MachineId }),
|
|
7270
|
-
to: looseObject({ device: MachineId })
|
|
7271
|
-
})
|
|
7272
|
-
});
|
|
7273
|
-
DirectAckRecord = object({ t: literal("ack"), id: Ulid });
|
|
7274
|
-
DirectNackRecord = object({ t: literal("nack"), id: Ulid, code: ErrorCode });
|
|
7275
|
-
DirectRecord = discriminatedUnion("t", [DirectSendRecord, DirectAckRecord, DirectNackRecord]);
|
|
7276
|
-
});
|
|
7277
|
-
|
|
7278
7174
|
// packages/core/dist/schema/ipc/device.js
|
|
7279
|
-
var BindingState, RealmAlarmSummary, DeviceStatus, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, DEVICE_METHODS;
|
|
7175
|
+
var BindingState, RealmAlarmSummary, DeviceStatus, DeviceChangeView, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, IdentityReplaceParams, SignInStep, DirectoryEraseParams, DEVICE_METHODS;
|
|
7280
7176
|
var init_device = __esm(() => {
|
|
7281
7177
|
init_zod();
|
|
7282
7178
|
init_common();
|
|
7283
7179
|
init_directory();
|
|
7284
7180
|
init_errors3();
|
|
7285
7181
|
init_jelly();
|
|
7286
|
-
BindingState = _enum(["none", "bound", "attest-due", "removed"]);
|
|
7182
|
+
BindingState = _enum(["none", "bound", "attest-due", "removed", "replace"]);
|
|
7287
7183
|
RealmAlarmSummary = object({
|
|
7288
7184
|
kind: _enum(["directory", "restored"]),
|
|
7289
7185
|
reason: string2().max(500),
|
|
@@ -7298,6 +7194,16 @@ var init_device = __esm(() => {
|
|
|
7298
7194
|
sth: object({ size: number2().int().positive(), root: Hash, ts: Millis }).optional(),
|
|
7299
7195
|
alarm: RealmAlarmSummary.optional()
|
|
7300
7196
|
});
|
|
7197
|
+
DeviceChangeView = object({
|
|
7198
|
+
change: _enum(["added", "vouched", "removed", "replaced"]),
|
|
7199
|
+
device: MachineId,
|
|
7200
|
+
label: MachineLabel,
|
|
7201
|
+
kind: DeviceKind.optional(),
|
|
7202
|
+
vouch: _enum(["jelly", "device", "admin"]),
|
|
7203
|
+
via: _enum(["host", "browser"]).optional(),
|
|
7204
|
+
approver: MachineLabel.optional(),
|
|
7205
|
+
at: Millis
|
|
7206
|
+
});
|
|
7301
7207
|
DeviceListEntry = object({
|
|
7302
7208
|
device: MachineId,
|
|
7303
7209
|
label: MachineLabel,
|
|
@@ -7314,10 +7220,9 @@ var init_device = __esm(() => {
|
|
|
7314
7220
|
LinkPreview = object({ device: MachineId, label: MachineLabel, kind: DeviceKind, sas: Sas });
|
|
7315
7221
|
KeyFingerprint = string2().regex(/^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$/, "key fingerprint");
|
|
7316
7222
|
LoginUserCode = string2().regex(/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/, "user code");
|
|
7317
|
-
LoginPurpose = _enum(["bind", "remove"]);
|
|
7223
|
+
LoginPurpose = _enum(["bind", "remove", "replace", "erase"]);
|
|
7318
7224
|
LoginKind = _enum(["laptop", "server", "other"]);
|
|
7319
7225
|
LoginShown = object({
|
|
7320
|
-
verificationUriComplete: string2().url().max(2048),
|
|
7321
7226
|
verificationUri: string2().url().max(2048),
|
|
7322
7227
|
userCode: LoginUserCode,
|
|
7323
7228
|
fingerprint: KeyFingerprint,
|
|
@@ -7332,6 +7237,14 @@ var init_device = __esm(() => {
|
|
|
7332
7237
|
]);
|
|
7333
7238
|
DeviceRemoveParams = strictObject({ device: MachineId, reason: _enum(["self", "removed", "lost"]).optional(), token: Jws.optional() });
|
|
7334
7239
|
DeviceApproveParams = strictObject({ link: LinkText, sas: Sas });
|
|
7240
|
+
IdentityReplaceParams = strictObject({
|
|
7241
|
+
token: Jws.optional(),
|
|
7242
|
+
label: MachineLabel.optional()
|
|
7243
|
+
});
|
|
7244
|
+
SignInStep = LoginShown.extend({ state: literal("sign-in"), purpose: LoginPurpose, interval: number2().int().positive() });
|
|
7245
|
+
DirectoryEraseParams = strictObject({
|
|
7246
|
+
token: Jws.optional()
|
|
7247
|
+
});
|
|
7335
7248
|
DEVICE_METHODS = {
|
|
7336
7249
|
"device.status": { params: strictObject({}), result: DeviceStatus },
|
|
7337
7250
|
"device.bind": {
|
|
@@ -7361,6 +7274,47 @@ var init_device = __esm(() => {
|
|
|
7361
7274
|
"device.approve": {
|
|
7362
7275
|
params: DeviceApproveParams,
|
|
7363
7276
|
result: object({ device: MachineId, seq: number2().int().nonnegative() })
|
|
7277
|
+
},
|
|
7278
|
+
"identity.replace": {
|
|
7279
|
+
params: IdentityReplaceParams,
|
|
7280
|
+
result: discriminatedUnion("state", [
|
|
7281
|
+
object({ state: literal("done"), person: PersonId, device: MachineId, predecessor: MachineId, seq: number2().int().nonnegative() }),
|
|
7282
|
+
object({ state: literal("token"), device: MachineId }),
|
|
7283
|
+
SignInStep.extend({ device: MachineId })
|
|
7284
|
+
])
|
|
7285
|
+
},
|
|
7286
|
+
"identity.replace.preview": {
|
|
7287
|
+
params: IdentityReplaceParams,
|
|
7288
|
+
result: object({
|
|
7289
|
+
predecessor: object({
|
|
7290
|
+
device: MachineId,
|
|
7291
|
+
label: MachineLabel,
|
|
7292
|
+
kind: DeviceKind.optional(),
|
|
7293
|
+
vouch: _enum(["jelly", "device"]),
|
|
7294
|
+
via: _enum(["host", "browser"]).optional(),
|
|
7295
|
+
since: Millis.optional(),
|
|
7296
|
+
current: boolean2()
|
|
7297
|
+
}),
|
|
7298
|
+
alarm: _enum(["keys_missing", "restored"]).nullable(),
|
|
7299
|
+
device: MachineId.optional(),
|
|
7300
|
+
signIns: number2().int().min(0).max(2),
|
|
7301
|
+
sentence: string2().max(600)
|
|
7302
|
+
})
|
|
7303
|
+
},
|
|
7304
|
+
"directory.erase": {
|
|
7305
|
+
params: DirectoryEraseParams,
|
|
7306
|
+
result: discriminatedUnion("state", [object({ state: literal("done"), person: PersonId, devices: number2().int().nonnegative() }), SignInStep])
|
|
7307
|
+
},
|
|
7308
|
+
"directory.erase.preview": {
|
|
7309
|
+
params: DirectoryEraseParams,
|
|
7310
|
+
result: object({
|
|
7311
|
+
person: PersonId,
|
|
7312
|
+
devices: array(object({ device: MachineId, label: MachineLabel, kind: DeviceKind.optional(), self: boolean2() })),
|
|
7313
|
+
signIns: number2().int().min(0).max(1),
|
|
7314
|
+
perDay: number2().int().positive(),
|
|
7315
|
+
lost: array(string2().max(300)).max(10),
|
|
7316
|
+
kept: array(string2().max(300)).max(10)
|
|
7317
|
+
})
|
|
7364
7318
|
}
|
|
7365
7319
|
};
|
|
7366
7320
|
});
|
|
@@ -7541,7 +7495,7 @@ var init_consent2 = __esm(() => {
|
|
|
7541
7495
|
HeldRequest = union([AskRequest, ContactRequestView, ContactSuggestion]);
|
|
7542
7496
|
RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
|
|
7543
7497
|
RequestText = string2().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
|
|
7544
|
-
PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional() });
|
|
7498
|
+
PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional(), devicesChangedAt: Millis.optional() });
|
|
7545
7499
|
RequestAnswerParams = strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() });
|
|
7546
7500
|
ContactRequestParams = strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() });
|
|
7547
7501
|
CONSENT_METHODS = {
|
|
@@ -7586,6 +7540,7 @@ var init_surfaces = __esm(() => {
|
|
|
7586
7540
|
init_directory_frames();
|
|
7587
7541
|
init_errors3();
|
|
7588
7542
|
init_consent2();
|
|
7543
|
+
init_device();
|
|
7589
7544
|
init_shapes();
|
|
7590
7545
|
ConversationRef = string2().regex(/^(?:dm:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})|channel:[a-z][a-z0-9-]{0,31}|agent:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})\/[a-z0-9][a-z0-9._-]{0,63})$/, "dm:<person>, channel:<name> or agent:<person>/<agent>");
|
|
7591
7546
|
ConvPlace = union([ChatSeq, ConvCursor]);
|
|
@@ -7626,7 +7581,6 @@ var init_surfaces = __esm(() => {
|
|
|
7626
7581
|
referenceTaskIds: array(Ulid).max(16).optional(),
|
|
7627
7582
|
wake: boolean2().optional().describe("hint that the recipient may wake an idle session; default true"),
|
|
7628
7583
|
request: strictObject({ contact: Email }).optional(),
|
|
7629
|
-
external: strictObject({ principal: string2().min(1).max(64) }).optional(),
|
|
7630
7584
|
onBehalfOf: strictObject({
|
|
7631
7585
|
taskId: Ulid.describe("a task this session executes"),
|
|
7632
7586
|
scope: array(DelegationScope).max(LIMITS.delegationScopes).optional().describe("narrow the scope; never wider than the task's"),
|
|
@@ -7690,13 +7644,16 @@ var init_surfaces = __esm(() => {
|
|
|
7690
7644
|
},
|
|
7691
7645
|
"people.get": {
|
|
7692
7646
|
params: strictObject({ person: PersonId }),
|
|
7693
|
-
result: PersonSummary.extend({
|
|
7647
|
+
result: PersonSummary.extend({
|
|
7648
|
+
devices: array(object({ device: MachineId, label: MachineLabel, self: boolean2(), away: literal(true).optional() })),
|
|
7649
|
+
deviceChanges: array(DeviceChangeView).max(10).optional()
|
|
7650
|
+
})
|
|
7694
7651
|
}
|
|
7695
7652
|
};
|
|
7696
7653
|
});
|
|
7697
7654
|
|
|
7698
7655
|
// packages/core/dist/schema/ipc/jelly.js
|
|
7699
|
-
var NoticeId, NOTICE_LINE_CHARS = 120, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
|
|
7656
|
+
var NoticeId, NOTICE_LINE_CHARS = 120, NoticeDevice, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
|
|
7700
7657
|
var init_jelly2 = __esm(() => {
|
|
7701
7658
|
init_zod();
|
|
7702
7659
|
init_common();
|
|
@@ -7707,12 +7664,18 @@ var init_jelly2 = __esm(() => {
|
|
|
7707
7664
|
init_shapes();
|
|
7708
7665
|
init_surfaces();
|
|
7709
7666
|
NoticeId = string2().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
|
|
7667
|
+
NoticeDevice = DeviceChangeView.extend({
|
|
7668
|
+
replaces: object({ device: MachineId, label: MachineLabel }).optional(),
|
|
7669
|
+
current: boolean2(),
|
|
7670
|
+
remove: _enum(["token", "sign-in", "team"]).optional()
|
|
7671
|
+
});
|
|
7710
7672
|
Notice = object({
|
|
7711
7673
|
id: NoticeId,
|
|
7712
7674
|
category: _enum(["message", "request", "alert"]),
|
|
7713
7675
|
conversation: ConversationRef.optional(),
|
|
7714
7676
|
from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }).optional(),
|
|
7715
7677
|
line: string2().max(NOTICE_LINE_CHARS).optional(),
|
|
7678
|
+
device: NoticeDevice.optional(),
|
|
7716
7679
|
at: Millis
|
|
7717
7680
|
});
|
|
7718
7681
|
NoticeCounts = object({ unread: number2().int().nonnegative(), requests: number2().int().nonnegative() });
|
|
@@ -7852,7 +7815,6 @@ var init_ipc = __esm(() => {
|
|
|
7852
7815
|
InviteSummary = object({
|
|
7853
7816
|
inviteId: Ulid,
|
|
7854
7817
|
person: PersonName,
|
|
7855
|
-
role: _enum(["member", "service"]),
|
|
7856
7818
|
expires: Millis,
|
|
7857
7819
|
state: _enum(["open", "redeemed", "revoked", "expired"])
|
|
7858
7820
|
});
|
|
@@ -7863,7 +7825,7 @@ var init_ipc = __esm(() => {
|
|
|
7863
7825
|
"session.register": {
|
|
7864
7826
|
params: strictObject({
|
|
7865
7827
|
key: SessionKey,
|
|
7866
|
-
agent: AgentKind.describe('client kind, e.g. "opencode", "claude-code"
|
|
7828
|
+
agent: AgentKind.describe('client kind, e.g. "opencode", "claude-code"'),
|
|
7867
7829
|
name: SessionName.optional().describe("requested name; the daemon may suffix it to keep it unique"),
|
|
7868
7830
|
cwd: string2().max(1024).optional(),
|
|
7869
7831
|
repo: string2().max(200).optional(),
|
|
@@ -7943,7 +7905,7 @@ var init_ipc = __esm(() => {
|
|
|
7943
7905
|
result: object({ team: TeamId, name: TeamName, machine: MachineId })
|
|
7944
7906
|
},
|
|
7945
7907
|
"team.invite": {
|
|
7946
|
-
params: strictObject({ person: PersonName,
|
|
7908
|
+
params: strictObject({ person: PersonName, ttlMs: number2().int().min(60000).max(LIMITS.ttlMs).optional() }),
|
|
7947
7909
|
result: object({ ticket: string2(), inviteId: Ulid, expires: Millis })
|
|
7948
7910
|
},
|
|
7949
7911
|
"team.join": {
|
|
@@ -7967,8 +7929,7 @@ var init_ipc = __esm(() => {
|
|
|
7967
7929
|
machine: MachineId,
|
|
7968
7930
|
enc: EncKey,
|
|
7969
7931
|
person: PersonName,
|
|
7970
|
-
label: MachineLabel
|
|
7971
|
-
role: _enum(["member", "service"]).optional()
|
|
7932
|
+
label: MachineLabel
|
|
7972
7933
|
}),
|
|
7973
7934
|
result: Ok
|
|
7974
7935
|
},
|
|
@@ -8061,6 +8022,8 @@ var init_ipc = __esm(() => {
|
|
|
8061
8022
|
"notice.ack",
|
|
8062
8023
|
"device.approve.preview",
|
|
8063
8024
|
"device.remove.preview",
|
|
8025
|
+
"identity.replace.preview",
|
|
8026
|
+
"directory.erase.preview",
|
|
8064
8027
|
"channel.create",
|
|
8065
8028
|
"channel.invite",
|
|
8066
8029
|
"channel.leave",
|
|
@@ -8081,6 +8044,8 @@ var init_ipc = __esm(() => {
|
|
|
8081
8044
|
"device.login.cancel",
|
|
8082
8045
|
"device.approve",
|
|
8083
8046
|
"identity.rotateEnc",
|
|
8047
|
+
"identity.replace",
|
|
8048
|
+
"directory.erase",
|
|
8084
8049
|
"team.create",
|
|
8085
8050
|
"team.invite",
|
|
8086
8051
|
"team.join",
|
|
@@ -8130,7 +8095,6 @@ var init_schema = __esm(() => {
|
|
|
8130
8095
|
init_roster();
|
|
8131
8096
|
init_relay();
|
|
8132
8097
|
init_directory_frames();
|
|
8133
|
-
init_direct();
|
|
8134
8098
|
init_ipc();
|
|
8135
8099
|
init_device();
|
|
8136
8100
|
init_consent2();
|
|
@@ -8161,7 +8125,7 @@ var init_b64u = __esm(() => {
|
|
|
8161
8125
|
});
|
|
8162
8126
|
|
|
8163
8127
|
// packages/core/dist/version.js
|
|
8164
|
-
var VERSION = "0.
|
|
8128
|
+
var VERSION = "0.7.0";
|
|
8165
8129
|
|
|
8166
8130
|
// packages/core/dist/jcs.js
|
|
8167
8131
|
function hasLoneSurrogate(text) {
|
|
@@ -8880,7 +8844,7 @@ function assertMeaning(type, from, to, klass, payload) {
|
|
|
8880
8844
|
throw new MessagingError("invalid_request", "a self sync goes between devices of one person");
|
|
8881
8845
|
if (p.conv?.kind === "card" && (from?.person === to.person || from?.agent !== undefined))
|
|
8882
8846
|
throw new MessagingError("invalid_request", "a card goes from a person to another person");
|
|
8883
|
-
const line = p.kind === "message" && p.
|
|
8847
|
+
const line = p.kind === "message" && p.message.taskId === undefined && Ulid.safeParse(p.message.messageId).success && (p.message.contextId === undefined || Ulid.safeParse(p.message.contextId).success);
|
|
8884
8848
|
if (!line)
|
|
8885
8849
|
throw new MessagingError("invalid_request", "a chat line is kind message, outside any task, with ULID messageId and contextId");
|
|
8886
8850
|
}
|
|
@@ -9192,7 +9156,7 @@ class Roster {
|
|
|
9192
9156
|
if (!verify(invite.invitePub, DOMAIN.invite, redeemClaim(this.team, inviteId, member), proof)) {
|
|
9193
9157
|
throw new MessagingError("invite_invalid", "invite proof does not verify for this member");
|
|
9194
9158
|
}
|
|
9195
|
-
if (member.person !== invite.person || member.role !==
|
|
9159
|
+
if (member.person !== invite.person || member.role !== "member") {
|
|
9196
9160
|
throw new MessagingError("invite_invalid", "member does not match the invite");
|
|
9197
9161
|
}
|
|
9198
9162
|
this.#requireNew(member);
|
|
@@ -9328,14 +9292,12 @@ function createInvite(roster, identity, args) {
|
|
|
9328
9292
|
inviteId,
|
|
9329
9293
|
secret,
|
|
9330
9294
|
person: args.person,
|
|
9331
|
-
role: args.role,
|
|
9332
9295
|
expires
|
|
9333
9296
|
});
|
|
9334
9297
|
const entry = createEntry(roster, identity, "invite.create", {
|
|
9335
9298
|
inviteId,
|
|
9336
9299
|
invitePub: rawPublicKey(seedKey(secret)),
|
|
9337
9300
|
person: args.person,
|
|
9338
|
-
role: args.role,
|
|
9339
9301
|
expires
|
|
9340
9302
|
}, now);
|
|
9341
9303
|
return { entry, ticket, secret };
|
|
@@ -9357,7 +9319,7 @@ function decodeTicket(text) {
|
|
|
9357
9319
|
function createRedeem(roster, ticket, identity, args) {
|
|
9358
9320
|
if (ticket.team !== roster.team)
|
|
9359
9321
|
throw new MessagingError("invite_invalid", "ticket is for another team");
|
|
9360
|
-
const member = { machine: identity.machine, enc: identity.enc, person: ticket.person, role:
|
|
9322
|
+
const member = { machine: identity.machine, enc: identity.enc, person: ticket.person, role: "member", label: args.label };
|
|
9361
9323
|
const proof = signWith(seedKey(ticket.secret), DOMAIN.invite, redeemClaim(roster.team, ticket.inviteId, member));
|
|
9362
9324
|
return createEntry(roster, identity, "invite.redeem", { inviteId: ticket.inviteId, proof, member }, args.now);
|
|
9363
9325
|
}
|
|
@@ -9970,6 +9932,13 @@ class DeviceLog {
|
|
|
9970
9932
|
token(seq) {
|
|
9971
9933
|
return this.#state.tokens.get(seq);
|
|
9972
9934
|
}
|
|
9935
|
+
spentSignIn(jti) {
|
|
9936
|
+
for (const [seq, token] of this.#state.tokens) {
|
|
9937
|
+
if (token.claims.src === JELLY_DEVICE_SOURCE && token.claims.jti === jti && this.#state.entries[seq].op !== "unbind")
|
|
9938
|
+
return true;
|
|
9939
|
+
}
|
|
9940
|
+
return false;
|
|
9941
|
+
}
|
|
9973
9942
|
bindingDeadline(seq) {
|
|
9974
9943
|
const token = this.#state.tokens.get(seq);
|
|
9975
9944
|
return token === undefined ? undefined : token.claims.exp * 1000 + LIMITS.clockSkewMs;
|
|
@@ -10022,6 +9991,14 @@ class DeviceLog {
|
|
|
10022
9991
|
const { claims } = token;
|
|
10023
9992
|
if (personId(claims.iss, claims.sub) !== this.person)
|
|
10024
9993
|
throw refuse("Jelly token names another person");
|
|
9994
|
+
if (claims.src === JELLY_DEVICE_SOURCE) {
|
|
9995
|
+
for (const [seq, used] of state.tokens) {
|
|
9996
|
+
if (used.claims.src !== JELLY_DEVICE_SOURCE || used.claims.jti !== claims.jti)
|
|
9997
|
+
continue;
|
|
9998
|
+
if (parsed.op !== "unbind" || state.entries[seq].op !== "unbind")
|
|
9999
|
+
throw refuse("a sign-in's token vouches for one act: this one was used");
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10025
10002
|
if (parsed.ts < claims.iat * 1000 - LIMITS.clockSkewMs || parsed.ts > claims.exp * 1000 + LIMITS.clockSkewMs)
|
|
10026
10003
|
throw refuse("log entry is dated outside its Jelly token's life");
|
|
10027
10004
|
tokens.set(parsed.seq, token);
|
|
@@ -10106,6 +10083,7 @@ var init_device_log = __esm(() => {
|
|
|
10106
10083
|
init_keys();
|
|
10107
10084
|
init_sign();
|
|
10108
10085
|
init_common();
|
|
10086
|
+
init_jelly();
|
|
10109
10087
|
init_directory();
|
|
10110
10088
|
init_errors3();
|
|
10111
10089
|
init_jelly_token();
|
|
@@ -10167,6 +10145,8 @@ function page(url, issuer) {
|
|
|
10167
10145
|
}
|
|
10168
10146
|
if (at.origin !== new URL(issuer).origin)
|
|
10169
10147
|
throw new MessagingError("jelly_unavailable", "Jelly ID's sign-in page is not on its own origin");
|
|
10148
|
+
if (at.search !== "" || at.hash !== "" || at.username !== "" || at.password !== "")
|
|
10149
|
+
throw new MessagingError("jelly_unavailable", "Jelly ID's sign-in page carries a query or credentials");
|
|
10170
10150
|
return at.href;
|
|
10171
10151
|
}
|
|
10172
10152
|
function readDeviceCodeAnswer(status, text, issuer) {
|
|
@@ -10175,9 +10155,7 @@ function readDeviceCodeAnswer(status, text, issuer) {
|
|
|
10175
10155
|
if (!parsed.success)
|
|
10176
10156
|
throw new MessagingError("jelly_unavailable", "Jelly ID's sign-in code could not be read");
|
|
10177
10157
|
const a = parsed.data;
|
|
10178
|
-
|
|
10179
|
-
const plain = page(a.verification_uri, issuer);
|
|
10180
|
-
return { deviceCode: a.device_code, userCode: a.user_code, verificationUri: plain, verificationUriComplete: complete, expiresInS: a.expires_in, intervalS: a.interval ?? LOGIN_INTERVAL_DEFAULT_S };
|
|
10158
|
+
return { deviceCode: a.device_code, userCode: a.user_code, verificationUri: page(a.verification_uri, issuer), expiresInS: a.expires_in, intervalS: a.interval };
|
|
10181
10159
|
}
|
|
10182
10160
|
const answer = errorOf(text);
|
|
10183
10161
|
switch (answer?.error) {
|
|
@@ -10217,8 +10195,6 @@ function readDevicePoll(status, text) {
|
|
|
10217
10195
|
case "invalid_client":
|
|
10218
10196
|
case "unsupported_grant_type":
|
|
10219
10197
|
throw unsupported("it refused this device client");
|
|
10220
|
-
case "invalid_request":
|
|
10221
|
-
throw new MessagingError("invalid_request", "Jelly ID refused the poll as malformed");
|
|
10222
10198
|
default:
|
|
10223
10199
|
return { kind: "retry" };
|
|
10224
10200
|
}
|
|
@@ -10236,7 +10212,7 @@ function deviceCodeForm(args) {
|
|
|
10236
10212
|
function devicePollForm(args) {
|
|
10237
10213
|
return new URLSearchParams({ grant_type: DEVICE_GRANT_TYPE, device_code: args.deviceCode, client_id: args.clientId });
|
|
10238
10214
|
}
|
|
10239
|
-
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code", LOGIN_SLOW_DOWN_S = 5,
|
|
10215
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code", LOGIN_SLOW_DOWN_S = 5, LOGIN_INTERVAL_MAX_S = 60, JELLY_ANSWER_BYTES = 65536, ResourceName, Metadata, unsupported = (why) => new MessagingError("login_unsupported", `Jelly ID does not offer device sign-in yet: ${why}`), CodeAnswer, ErrorAnswer, TokenAnswer;
|
|
10240
10216
|
var init_jelly_device = __esm(() => {
|
|
10241
10217
|
init_b64u();
|
|
10242
10218
|
init_keys();
|
|
@@ -10251,12 +10227,11 @@ var init_jelly_device = __esm(() => {
|
|
|
10251
10227
|
jelly_resource_device_token_endpoint: string2().max(2048).optional()
|
|
10252
10228
|
});
|
|
10253
10229
|
CodeAnswer = looseObject({
|
|
10254
|
-
device_code: string2().min(
|
|
10230
|
+
device_code: string2().min(22).max(256),
|
|
10255
10231
|
user_code: string2().regex(/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/),
|
|
10256
10232
|
verification_uri: string2().max(2048),
|
|
10257
|
-
verification_uri_complete: string2().max(2048),
|
|
10258
10233
|
expires_in: number2().int().positive().max(3600),
|
|
10259
|
-
interval: number2().int().positive().max(LOGIN_INTERVAL_MAX_S)
|
|
10234
|
+
interval: number2().int().positive().max(LOGIN_INTERVAL_MAX_S)
|
|
10260
10235
|
});
|
|
10261
10236
|
ErrorAnswer = looseObject({ error: string2().max(64), detail: string2().max(64).optional() });
|
|
10262
10237
|
TokenAnswer = looseObject({ access_token: string2().min(1).max(8192), token_type: string2().max(32) });
|
|
@@ -10655,6 +10630,31 @@ function verifyPersonView(view, context) {
|
|
|
10655
10630
|
throw asMessagingError(error, "person view");
|
|
10656
10631
|
}
|
|
10657
10632
|
}
|
|
10633
|
+
function checkPersonSuccession(earlier, later) {
|
|
10634
|
+
if (earlier.person !== later.person)
|
|
10635
|
+
throw new MessagingError("invalid_request", "the two states are of two persons");
|
|
10636
|
+
if (earlier.erased || later.erased)
|
|
10637
|
+
return;
|
|
10638
|
+
const ours = earlier.log.entries();
|
|
10639
|
+
const theirs = later.log.entries();
|
|
10640
|
+
if (theirs.length < ours.length)
|
|
10641
|
+
throw new DirectoryInconsistency("a later tree head serves a shorter log", { kind: "rollback", what: "log", ours: ours.length, theirs: theirs.length });
|
|
10642
|
+
for (let seq = 0;seq < ours.length; seq++) {
|
|
10643
|
+
if (logEntryHash(ours[seq]) !== logEntryHash(theirs[seq]))
|
|
10644
|
+
throw new DirectoryInconsistency("the log differs from the one held", { kind: "fork", seq, ours: ours[seq], theirs: theirs[seq] });
|
|
10645
|
+
}
|
|
10646
|
+
for (const [device, statement] of earlier.keys) {
|
|
10647
|
+
if (!later.log.device(device)?.current)
|
|
10648
|
+
continue;
|
|
10649
|
+
const next = later.keys.get(device);
|
|
10650
|
+
if (!next)
|
|
10651
|
+
throw new DirectoryInconsistency("a held keys statement is withheld", { kind: "rollback", what: "keys", ours: statement.seq, theirs: -1 });
|
|
10652
|
+
if (next.seq < statement.seq)
|
|
10653
|
+
throw new DirectoryInconsistency("a keys statement is older than the one held", { kind: "rollback", what: "keys", ours: statement.seq, theirs: next.seq });
|
|
10654
|
+
if (next.seq === statement.seq && keysStatementHash(next) !== keysStatementHash(statement))
|
|
10655
|
+
throw fail("two keys statements share a device and seq");
|
|
10656
|
+
}
|
|
10657
|
+
}
|
|
10658
10658
|
function checkWitness(witness, seq, deadline, log, person, sth) {
|
|
10659
10659
|
const epoch = verifyEpochProof(sth, witness.epoch);
|
|
10660
10660
|
const committed = witness.value.log;
|
|
@@ -10665,6 +10665,33 @@ function checkWitness(witness, seq, deadline, log, person, sth) {
|
|
|
10665
10665
|
if (epoch.leaf.ts > deadline)
|
|
10666
10666
|
throw new MessagingError("binding_invalid", "a Jelly binding first appeared after its token's life");
|
|
10667
10667
|
}
|
|
10668
|
+
function verifyPersonHead(view, context) {
|
|
10669
|
+
try {
|
|
10670
|
+
const parsed = parseWith(PersonHead, view, "person head");
|
|
10671
|
+
const { sth } = context;
|
|
10672
|
+
const head = verifyEpochProof(sth, context.head);
|
|
10673
|
+
if (head.index !== sth.size - 1)
|
|
10674
|
+
throw fail("the head epoch is not the tree head's last");
|
|
10675
|
+
const mapRoot = head.leaf.mapRoot;
|
|
10676
|
+
if (parsed.tombstone) {
|
|
10677
|
+
if (parsed.value)
|
|
10678
|
+
throw new MessagingError("invalid_request", "an erased person has no head");
|
|
10679
|
+
if (!verifyMapProof(mapRoot, parsed.person, TOMBSTONE, parsed.mapProof))
|
|
10680
|
+
throw new DirectoryInconsistency("the map does not hold the tombstone", { kind: "map", sth, head });
|
|
10681
|
+
return { person: parsed.person, state: "erased" };
|
|
10682
|
+
}
|
|
10683
|
+
if (!parsed.value) {
|
|
10684
|
+
if (!verifyMapProof(mapRoot, parsed.person, undefined, parsed.mapProof))
|
|
10685
|
+
throw new DirectoryInconsistency("the map holds a person served as absent", { kind: "map", sth, head });
|
|
10686
|
+
return { person: parsed.person, state: "absent" };
|
|
10687
|
+
}
|
|
10688
|
+
if (!verifyMapProof(mapRoot, parsed.person, mapValue(parsed.value), parsed.mapProof))
|
|
10689
|
+
throw new DirectoryInconsistency("the map does not commit to the head served", { kind: "map", sth, head });
|
|
10690
|
+
return { person: parsed.person, state: "live", log: parsed.value.log, keys: parsed.value.keys };
|
|
10691
|
+
} catch (error) {
|
|
10692
|
+
throw asMessagingError(error, "person head");
|
|
10693
|
+
}
|
|
10694
|
+
}
|
|
10668
10695
|
var DirectoryInconsistency, fail = (what) => new MessagingError("directory_inconsistent", what);
|
|
10669
10696
|
var init_person_view = __esm(() => {
|
|
10670
10697
|
init_check();
|
|
@@ -11016,8 +11043,6 @@ function grant(identity, args) {
|
|
|
11016
11043
|
const raw = args.parent;
|
|
11017
11044
|
const parent = raw === undefined ? undefined : parseWith(DelegationGrant, raw, "parent grant");
|
|
11018
11045
|
if (parent !== undefined) {
|
|
11019
|
-
if (args.external !== undefined)
|
|
11020
|
-
throw new MessagingError("invalid_request", "only a root grant names an external principal");
|
|
11021
11046
|
if (parent.to !== args.from)
|
|
11022
11047
|
throw new MessagingError("invalid_request", "a grant extends the grant to its delegating session");
|
|
11023
11048
|
}
|
|
@@ -11042,8 +11067,7 @@ function grant(identity, args) {
|
|
|
11042
11067
|
exp,
|
|
11043
11068
|
maxDepth,
|
|
11044
11069
|
...scope !== undefined ? { scope: [...scope] } : {},
|
|
11045
|
-
...raw !== undefined ? { parent: grantHash(raw) } : {}
|
|
11046
|
-
...args.external !== undefined ? { external: { principal: args.external.principal } } : {}
|
|
11070
|
+
...raw !== undefined ? { parent: grantHash(raw) } : {}
|
|
11047
11071
|
};
|
|
11048
11072
|
parseWith(DelegationGrant.omit({ sig: true }), unsigned, "delegation grant");
|
|
11049
11073
|
return { ...unsigned, sig: sign(identity, DOMAIN.delegation, unsigned) };
|
|
@@ -11065,7 +11089,7 @@ function check2(raw, ctx) {
|
|
|
11065
11089
|
throw new MessagingError("too_large", `a delegation chain is limited to ${LIMITS.delegationBytes} bytes`);
|
|
11066
11090
|
let maxDepth = LIMITS.delegationDepth;
|
|
11067
11091
|
let previous;
|
|
11068
|
-
for (const
|
|
11092
|
+
for (const grant of grants) {
|
|
11069
11093
|
const { sig, ...unsigned } = grant;
|
|
11070
11094
|
const input = signingInput(DOMAIN.delegation, unsigned);
|
|
11071
11095
|
if (grant.realm !== realm.realm)
|
|
@@ -11087,9 +11111,6 @@ function check2(raw, ctx) {
|
|
|
11087
11111
|
}
|
|
11088
11112
|
if (!verifyInput(grant.by, input, sig))
|
|
11089
11113
|
throw new MessagingError("bad_signature", "a grant signature is invalid");
|
|
11090
|
-
if (grant.external !== undefined && (index > 0 || signer.role !== "service")) {
|
|
11091
|
-
throw new MessagingError("forbidden", "only a service member's root grant names an external principal");
|
|
11092
|
-
}
|
|
11093
11114
|
if (previous === undefined) {
|
|
11094
11115
|
if (grant.parent !== undefined)
|
|
11095
11116
|
throw new MessagingError("invalid_request", "a root grant has no parent");
|
|
@@ -11118,9 +11139,6 @@ function check2(raw, ctx) {
|
|
|
11118
11139
|
if (last.by !== hop.by || last.from !== hop.from || last.to !== hop.to || last.task !== hop.task) {
|
|
11119
11140
|
throw new MessagingError("forbidden", "the chain does not authorize this request");
|
|
11120
11141
|
}
|
|
11121
|
-
const external = grants.length === 1 ? root.external?.principal : undefined;
|
|
11122
|
-
if (external !== hop.external)
|
|
11123
|
-
throw new MessagingError("forbidden", "the chain's external principal does not match the request");
|
|
11124
11142
|
return {
|
|
11125
11143
|
grants: structuredClone(grants),
|
|
11126
11144
|
summary: {
|
|
@@ -11128,7 +11146,6 @@ function check2(raw, ctx) {
|
|
|
11128
11146
|
depth: grants.length,
|
|
11129
11147
|
maxDepth,
|
|
11130
11148
|
...last.scope !== undefined ? { scope: [...last.scope] } : {},
|
|
11131
|
-
...root.external !== undefined ? { external: { principal: root.external.principal, gateway: root.by } } : {},
|
|
11132
11149
|
exp: Math.min(...grants.map((grant) => grant.exp))
|
|
11133
11150
|
}
|
|
11134
11151
|
};
|
|
@@ -11190,103 +11207,8 @@ var init_task = __esm(() => {
|
|
|
11190
11207
|
};
|
|
11191
11208
|
});
|
|
11192
11209
|
|
|
11193
|
-
// packages/core/dist/direct.js
|
|
11194
|
-
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash as createHash7, diffieHellman as diffieHellman2, generateKeyPairSync as generateKeyPairSync3, hkdfSync as hkdfSync2, randomBytes as randomBytes4 } from "crypto";
|
|
11195
|
-
function directEphemeral() {
|
|
11196
|
-
const key = generateKeyPairSync3("x25519").privateKey;
|
|
11197
|
-
return { key, half: { epk: rawPublicKey(key), nonce: b64u.encode(randomBytes4(32)) } };
|
|
11198
|
-
}
|
|
11199
|
-
function directTranscript(initiator, responder) {
|
|
11200
|
-
return { proto: DIRECT_PROTOCOL, initiator: { epk: initiator.epk, nonce: initiator.nonce }, responder: { epk: responder.epk, nonce: responder.nonce } };
|
|
11201
|
-
}
|
|
11202
|
-
function directAuthPayload(args) {
|
|
11203
|
-
const { transcript, team, role, machine, peer } = args;
|
|
11204
|
-
if (role === "initiator" !== (peer !== undefined))
|
|
11205
|
-
throw new MessagingError("invalid_request", "only the initiator names its peer");
|
|
11206
|
-
return { ...transcript, team, role, machine, ...peer === undefined ? {} : { peer } };
|
|
11207
|
-
}
|
|
11208
|
-
function directKeys(own, peerEpk, transcript) {
|
|
11209
|
-
let shared;
|
|
11210
|
-
try {
|
|
11211
|
-
shared = diffieHellman2({ privateKey: own, publicKey: publicKeyFromRaw("x25519", peerEpk) });
|
|
11212
|
-
} catch {
|
|
11213
|
-
throw new MessagingError("bad_signature", "the peer's handshake key is not usable");
|
|
11214
|
-
}
|
|
11215
|
-
if (shared.every((byte) => byte === 0))
|
|
11216
|
-
throw new MessagingError("bad_signature", "the peer's handshake key is not usable");
|
|
11217
|
-
const salt = createHash7("sha256").update(canonicalize(transcript)).digest();
|
|
11218
|
-
const okm = new Uint8Array(hkdfSync2("sha256", shared, salt, DOMAIN.directLink, 128));
|
|
11219
|
-
return {
|
|
11220
|
-
box: { responder: okm.slice(0, 32), initiator: okm.slice(32, 64) },
|
|
11221
|
-
record: { initiator: okm.slice(64, 96), responder: okm.slice(96, 128) }
|
|
11222
|
-
};
|
|
11223
|
-
}
|
|
11224
|
-
function sealDirectBox(key, transcript, identity) {
|
|
11225
|
-
const cipher = createCipheriv2("aes-256-gcm", key, ZERO_NONCE, { authTagLength: TAG_BYTES2 });
|
|
11226
|
-
cipher.setAAD(utf8(canonicalize(transcript)));
|
|
11227
|
-
return b64u.encode(Buffer.concat([cipher.update(utf8(canonicalize(identity))), cipher.final(), cipher.getAuthTag()]));
|
|
11228
|
-
}
|
|
11229
|
-
function openDirectBox(key, transcript, box) {
|
|
11230
|
-
let plaintext;
|
|
11231
|
-
try {
|
|
11232
|
-
const bytes = b64u.decode(box);
|
|
11233
|
-
if (bytes.length <= TAG_BYTES2)
|
|
11234
|
-
throw new Error("short");
|
|
11235
|
-
const decipher = createDecipheriv2("aes-256-gcm", key, ZERO_NONCE, { authTagLength: TAG_BYTES2 });
|
|
11236
|
-
decipher.setAAD(utf8(canonicalize(transcript)));
|
|
11237
|
-
decipher.setAuthTag(bytes.subarray(bytes.length - TAG_BYTES2));
|
|
11238
|
-
plaintext = Buffer.concat([decipher.update(bytes.subarray(0, bytes.length - TAG_BYTES2)), decipher.final()]);
|
|
11239
|
-
} catch {
|
|
11240
|
-
throw new MessagingError("bad_signature", "the handshake box does not open");
|
|
11241
|
-
}
|
|
11242
|
-
return parseWire(plaintext.toString("utf8"), { maxBytes: DIRECT_HANDSHAKE_BYTES });
|
|
11243
|
-
}
|
|
11244
|
-
|
|
11245
|
-
class DirectCipher {
|
|
11246
|
-
#key;
|
|
11247
|
-
#counter = 0;
|
|
11248
|
-
constructor(key) {
|
|
11249
|
-
if (key.length !== 32)
|
|
11250
|
-
throw new MessagingError("invalid_request", "a record key is 32 bytes");
|
|
11251
|
-
this.#key = key;
|
|
11252
|
-
}
|
|
11253
|
-
seal(plaintext) {
|
|
11254
|
-
const cipher = createCipheriv2("aes-256-gcm", this.#key, this.#nonce(), { authTagLength: TAG_BYTES2 });
|
|
11255
|
-
return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
11256
|
-
}
|
|
11257
|
-
open(record) {
|
|
11258
|
-
try {
|
|
11259
|
-
if (record.length < TAG_BYTES2)
|
|
11260
|
-
throw new Error("short");
|
|
11261
|
-
const decipher = createDecipheriv2("aes-256-gcm", this.#key, this.#nonce(), { authTagLength: TAG_BYTES2 });
|
|
11262
|
-
decipher.setAuthTag(record.subarray(record.length - TAG_BYTES2));
|
|
11263
|
-
return Buffer.concat([decipher.update(record.subarray(0, record.length - TAG_BYTES2)), decipher.final()]);
|
|
11264
|
-
} catch {
|
|
11265
|
-
throw new MessagingError("bad_signature", "a direct record does not open");
|
|
11266
|
-
}
|
|
11267
|
-
}
|
|
11268
|
-
#nonce() {
|
|
11269
|
-
if (this.#counter >= Number.MAX_SAFE_INTEGER)
|
|
11270
|
-
throw new MessagingError("internal", "record counter exhausted");
|
|
11271
|
-
const nonce = Buffer.alloc(NONCE_BYTES2);
|
|
11272
|
-
nonce.writeBigUInt64BE(BigInt(this.#counter++), 4);
|
|
11273
|
-
return nonce;
|
|
11274
|
-
}
|
|
11275
|
-
}
|
|
11276
|
-
var NONCE_BYTES2 = 12, TAG_BYTES2 = 16, ZERO_NONCE;
|
|
11277
|
-
var init_direct2 = __esm(() => {
|
|
11278
|
-
init_b64u();
|
|
11279
|
-
init_jcs();
|
|
11280
|
-
init_keys();
|
|
11281
|
-
init_common();
|
|
11282
|
-
init_direct();
|
|
11283
|
-
init_errors3();
|
|
11284
|
-
init_wire();
|
|
11285
|
-
ZERO_NONCE = new Uint8Array(NONCE_BYTES2);
|
|
11286
|
-
});
|
|
11287
|
-
|
|
11288
11210
|
// packages/core/dist/peer-text.js
|
|
11289
|
-
import { randomBytes as
|
|
11211
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
11290
11212
|
function addReading(code, letters) {
|
|
11291
11213
|
const c = String.fromCodePoint(code);
|
|
11292
11214
|
READINGS.set(c, (READINGS.get(c) ?? "") + letters);
|
|
@@ -11523,7 +11445,7 @@ function createFence(options) {
|
|
|
11523
11445
|
if (!TAG.test(tag))
|
|
11524
11446
|
throw new Error("fence tag must be lowercase words joined by hyphens");
|
|
11525
11447
|
const stem = tag.split("-")[0];
|
|
11526
|
-
const random = options.random ??
|
|
11448
|
+
const random = options.random ?? randomBytes4;
|
|
11527
11449
|
const opened = new RegExp(`<${tag}-[${NONCE_ALPHABET}]{${NONCE_LENGTH}}[ >]`);
|
|
11528
11450
|
const neutralize = (text, clean = {}) => render(text, { singleLine: clean.singleLine === true, maxChars: clean.maxChars, terminal: false, attribute: false, stem });
|
|
11529
11451
|
function fence(body, attributes = {}, clean = {}, block) {
|
|
@@ -12645,7 +12567,6 @@ var init_agent_tools = __esm(() => {
|
|
|
12645
12567
|
at: iso,
|
|
12646
12568
|
parts: array(Part2),
|
|
12647
12569
|
verified: _enum(["signature", "none"]).optional().describe("signature: the daemon checked the sender's signature"),
|
|
12648
|
-
external: fenced("An outside A2A principal it came from, through the gateway").optional(),
|
|
12649
12570
|
replyTo: string2().optional(),
|
|
12650
12571
|
chain: Chain.optional(),
|
|
12651
12572
|
task: Task2.optional(),
|
|
@@ -12775,7 +12696,6 @@ var init_dist = __esm(() => {
|
|
|
12775
12696
|
init_delegation2();
|
|
12776
12697
|
init_task();
|
|
12777
12698
|
init_a2a();
|
|
12778
|
-
init_direct2();
|
|
12779
12699
|
init_peer_text();
|
|
12780
12700
|
init_agent_tools();
|
|
12781
12701
|
init_operations();
|
|
@@ -12903,13 +12823,13 @@ var init_paths = __esm(() => {
|
|
|
12903
12823
|
});
|
|
12904
12824
|
|
|
12905
12825
|
// packages/ipc/dist/pipes.js
|
|
12906
|
-
import { randomBytes as
|
|
12826
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
12907
12827
|
import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
12908
12828
|
function newPipeName(tier) {
|
|
12909
|
-
return `\\\\.\\pipe\\messaging-${
|
|
12829
|
+
return `\\\\.\\pipe\\messaging-${randomBytes5(16).toString("hex")}-${tier}`;
|
|
12910
12830
|
}
|
|
12911
12831
|
function writePointer(path, pointer) {
|
|
12912
|
-
const temp = `${path}.${process.pid}.${
|
|
12832
|
+
const temp = `${path}.${process.pid}.${randomBytes5(4).toString("hex")}.tmp`;
|
|
12913
12833
|
try {
|
|
12914
12834
|
writeFileSync(temp, `${JSON.stringify(pointer)}
|
|
12915
12835
|
`, { flag: "wx" });
|
|
@@ -12965,7 +12885,7 @@ var init_rpc = __esm(() => {
|
|
|
12965
12885
|
|
|
12966
12886
|
// packages/ipc/dist/windows-acl.js
|
|
12967
12887
|
import { spawnSync } from "child_process";
|
|
12968
|
-
import { randomBytes as
|
|
12888
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
12969
12889
|
import { existsSync, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
|
|
12970
12890
|
import { tmpdir } from "os";
|
|
12971
12891
|
import { join as join2 } from "path";
|
|
@@ -13080,7 +13000,7 @@ function currentUserSid() {
|
|
|
13080
13000
|
return sid;
|
|
13081
13001
|
}
|
|
13082
13002
|
function readDacl(path) {
|
|
13083
|
-
const out = join2(tmpdir(), `messaging-acl-${process.pid}-${
|
|
13003
|
+
const out = join2(tmpdir(), `messaging-acl-${process.pid}-${randomBytes6(8).toString("hex")}.txt`);
|
|
13084
13004
|
try {
|
|
13085
13005
|
run("icacls.exe", [path, "/save", out, "/q"]);
|
|
13086
13006
|
return parseSddlDacl(sddlLine(readFileSync2(out)));
|
|
@@ -13859,85 +13779,6 @@ var init_dist2 = __esm(() => {
|
|
|
13859
13779
|
init_windows_acl();
|
|
13860
13780
|
});
|
|
13861
13781
|
|
|
13862
|
-
// packages/daemon/dist/direct-config.js
|
|
13863
|
-
import { BlockList, isIP } from "net";
|
|
13864
|
-
function parseHostPort(text) {
|
|
13865
|
-
const match = /^\[([0-9A-Fa-f:.]+)\](?::(\d{1,5}))?$/.exec(text) ?? /^([0-9.]+)(?::(\d{1,5}))?$/.exec(text);
|
|
13866
|
-
if (!match)
|
|
13867
|
-
return;
|
|
13868
|
-
const host = match[1];
|
|
13869
|
-
const family = isIP(host);
|
|
13870
|
-
if (family === 0 || family === 6 !== text.startsWith("["))
|
|
13871
|
-
return;
|
|
13872
|
-
const port = match[2] === undefined ? undefined : Number(match[2]);
|
|
13873
|
-
if (port !== undefined && port > 65535)
|
|
13874
|
-
return;
|
|
13875
|
-
return { host, port };
|
|
13876
|
-
}
|
|
13877
|
-
function family(host) {
|
|
13878
|
-
const f = isIP(host);
|
|
13879
|
-
return f === 4 ? "ipv4" : f === 6 ? "ipv6" : undefined;
|
|
13880
|
-
}
|
|
13881
|
-
function isLoopback(host) {
|
|
13882
|
-
const f = family(host);
|
|
13883
|
-
return f !== undefined && LOOPBACK.check(host, f);
|
|
13884
|
-
}
|
|
13885
|
-
function isWildcard(host) {
|
|
13886
|
-
return host === "0.0.0.0" || host === "::";
|
|
13887
|
-
}
|
|
13888
|
-
function isDirectHost(host) {
|
|
13889
|
-
const f = family(host);
|
|
13890
|
-
return f !== undefined && (LOOPBACK.check(host, f) || LAN.check(host, f));
|
|
13891
|
-
}
|
|
13892
|
-
function mayDial(endpoint, ownListenerOnLoopback) {
|
|
13893
|
-
if (!isDirectHost(endpoint.host))
|
|
13894
|
-
return false;
|
|
13895
|
-
return !isLoopback(endpoint.host) || ownListenerOnLoopback;
|
|
13896
|
-
}
|
|
13897
|
-
function advertisedEndpoints(config, bound) {
|
|
13898
|
-
if (config.advertise === undefined)
|
|
13899
|
-
return isWildcard(bound.host) ? [] : [{ host: bound.host, port: bound.port }];
|
|
13900
|
-
return config.advertise.map((text) => {
|
|
13901
|
-
const parsed = parseHostPort(text);
|
|
13902
|
-
return { host: parsed.host, port: parsed.port ?? bound.port };
|
|
13903
|
-
});
|
|
13904
|
-
}
|
|
13905
|
-
var DEFAULT_DIRECT_LISTEN = "127.0.0.1:0", LOOPBACK, LAN, ListenAddress, AdvertiseAddress, DirectConfig;
|
|
13906
|
-
var init_direct_config = __esm(() => {
|
|
13907
|
-
init_zod();
|
|
13908
|
-
LOOPBACK = new BlockList;
|
|
13909
|
-
LOOPBACK.addSubnet("127.0.0.0", 8, "ipv4");
|
|
13910
|
-
LOOPBACK.addAddress("::1", "ipv6");
|
|
13911
|
-
LAN = new BlockList;
|
|
13912
|
-
for (const [net, prefix] of [
|
|
13913
|
-
["10.0.0.0", 8],
|
|
13914
|
-
["172.16.0.0", 12],
|
|
13915
|
-
["192.168.0.0", 16],
|
|
13916
|
-
["100.64.0.0", 10],
|
|
13917
|
-
["169.254.0.0", 16]
|
|
13918
|
-
]) {
|
|
13919
|
-
LAN.addSubnet(net, prefix, "ipv4");
|
|
13920
|
-
}
|
|
13921
|
-
LAN.addSubnet("fe80::", 10, "ipv6");
|
|
13922
|
-
LAN.addSubnet("fc00::", 7, "ipv6");
|
|
13923
|
-
ListenAddress = string2().refine((text) => {
|
|
13924
|
-
const parsed = parseHostPort(text);
|
|
13925
|
-
return parsed?.port !== undefined && (isWildcard(parsed.host) || isDirectHost(parsed.host));
|
|
13926
|
-
}, "listen must be ip:port (IPv6 in brackets) on loopback, a private or link-local address, or all interfaces");
|
|
13927
|
-
AdvertiseAddress = string2().refine((text) => {
|
|
13928
|
-
const parsed = parseHostPort(text);
|
|
13929
|
-
return parsed !== undefined && parsed.port !== 0 && isDirectHost(parsed.host);
|
|
13930
|
-
}, "advertise entries must be ip or ip:port (IPv6 in brackets) on loopback or a private or link-local address");
|
|
13931
|
-
DirectConfig = object({
|
|
13932
|
-
enabled: boolean2().optional(),
|
|
13933
|
-
listen: ListenAddress.optional(),
|
|
13934
|
-
advertise: array(AdvertiseAddress).min(1).max(4).optional()
|
|
13935
|
-
}).strict().refine((c) => c.enabled !== true || c.advertise !== undefined || !isWildcard(parseHostPort(c.listen ?? DEFAULT_DIRECT_LISTEN).host), {
|
|
13936
|
-
message: "advertise is required when listening on all interfaces",
|
|
13937
|
-
path: ["advertise"]
|
|
13938
|
-
});
|
|
13939
|
-
});
|
|
13940
|
-
|
|
13941
13782
|
// packages/daemon/dist/identity/record.js
|
|
13942
13783
|
import { createPrivateKey as createPrivateKey2 } from "crypto";
|
|
13943
13784
|
import { closeSync as closeSync2, constants as constants3, existsSync as existsSync3, fstatSync, fsyncSync, linkSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, statSync, unlinkSync, writeSync } from "fs";
|
|
@@ -14214,7 +14055,6 @@ var init_home = __esm(() => {
|
|
|
14214
14055
|
init_zod();
|
|
14215
14056
|
init_dist();
|
|
14216
14057
|
init_dist2();
|
|
14217
|
-
init_direct_config();
|
|
14218
14058
|
init_record();
|
|
14219
14059
|
PRESENCE_FIELDS = ["repo", "branch", "doing"];
|
|
14220
14060
|
PresenceField = _enum(PRESENCE_FIELDS);
|
|
@@ -14225,8 +14065,6 @@ var init_home = __esm(() => {
|
|
|
14225
14065
|
label: MachineLabel.optional(),
|
|
14226
14066
|
realm: DirectoryRealmConfig.strict().optional(),
|
|
14227
14067
|
inbound: object({
|
|
14228
|
-
refuse: array(PersonName).max(0, { error: 'inbound.refuse was replaced by block grants (spec \xA76.9): remove it, and block each person with grant.set {subject: {person}, chat: "blocked"}' }).optional(),
|
|
14229
|
-
wake: literal("team", { error: "inbound.wake was removed: a push-tier session may be woken for what consent delivers to it; register sessions with tier pull to never wake them (spec \xA78.3)" }).optional(),
|
|
14230
14068
|
quota: object({ items: number2().int().min(1), bytes: number2().int().min(1) }).partial().strict().optional()
|
|
14231
14069
|
}).strict().optional(),
|
|
14232
14070
|
presence: object({ share: array(PresenceField).max(PRESENCE_FIELDS.length) }).strict().optional(),
|
|
@@ -14240,7 +14078,6 @@ var init_home = __esm(() => {
|
|
|
14240
14078
|
transcriptDays: number2().int().min(1).max(36500)
|
|
14241
14079
|
}).partial().strict().optional(),
|
|
14242
14080
|
ipc: object({ agentGroup: number2().int().min(0).max(4294967294) }).partial().strict().optional(),
|
|
14243
|
-
transport: object({ direct: DirectConfig.optional() }).strict().optional(),
|
|
14244
14081
|
identity: object({
|
|
14245
14082
|
store: _enum(["auto", "file", "keychain"]),
|
|
14246
14083
|
prompt: boolean2(),
|
|
@@ -14828,8 +14665,6 @@ function machineLoopGuard() {
|
|
|
14828
14665
|
function inboundPolicy(facts, state, now) {
|
|
14829
14666
|
if (!facts.recipient)
|
|
14830
14667
|
return "unknown_session";
|
|
14831
|
-
if (facts.recipient.agent === GATEWAY_AGENT)
|
|
14832
|
-
return "refused";
|
|
14833
14668
|
if (facts.task && facts.recipient.agent === CLI_AGENT)
|
|
14834
14669
|
return "refused";
|
|
14835
14670
|
const agents = facts.sender.agent !== CLI_AGENT && facts.recipient.agent !== CLI_AGENT;
|
|
@@ -14840,8 +14675,6 @@ ${facts.recipient.session}`, now))
|
|
|
14840
14675
|
if (agents && !state.machineLoop.take(`${facts.sender.machine}
|
|
14841
14676
|
${facts.recipient.session}`, now))
|
|
14842
14677
|
return "loop_guard";
|
|
14843
|
-
if (facts.external && facts.sender.role !== "service")
|
|
14844
|
-
return "forbidden";
|
|
14845
14678
|
return;
|
|
14846
14679
|
}
|
|
14847
14680
|
function isFinalStage(stage) {
|
|
@@ -14857,7 +14690,7 @@ function canAdvance(from, to) {
|
|
|
14857
14690
|
function inboxAdvances(from, to) {
|
|
14858
14691
|
return INBOX_RANK[to] > INBOX_RANK[from];
|
|
14859
14692
|
}
|
|
14860
|
-
var RESPONSE_RATE_FACTOR = 4, CHAT_SEND_PER_MACHINE_PER_MINUTE, DEFER_MARGIN_MS = 60000, RESPONSE_TYPES, LOOP_MACHINE_FACTOR = 2,
|
|
14693
|
+
var RESPONSE_RATE_FACTOR = 4, CHAT_SEND_PER_MACHINE_PER_MINUTE, DEFER_MARGIN_MS = 60000, RESPONSE_TYPES, LOOP_MACHINE_FACTOR = 2, STAGE_RANK, INBOX_RANK;
|
|
14861
14694
|
var init_policy = __esm(() => {
|
|
14862
14695
|
init_dist();
|
|
14863
14696
|
CHAT_SEND_PER_MACHINE_PER_MINUTE = LIMITS.inboundPerMachinePerMinute - 10;
|
|
@@ -14877,7 +14710,7 @@ var init_policy = __esm(() => {
|
|
|
14877
14710
|
});
|
|
14878
14711
|
|
|
14879
14712
|
// packages/daemon/dist/chat.js
|
|
14880
|
-
import { createHash as
|
|
14713
|
+
import { createHash as createHash7 } from "crypto";
|
|
14881
14714
|
|
|
14882
14715
|
class Chats {
|
|
14883
14716
|
#host;
|
|
@@ -14952,7 +14785,7 @@ class Chats {
|
|
|
14952
14785
|
}
|
|
14953
14786
|
send(p, session, scope = "") {
|
|
14954
14787
|
const room = p.conversation;
|
|
14955
|
-
const digest = p.clientId === undefined ? undefined :
|
|
14788
|
+
const digest = p.clientId === undefined ? undefined : createHash7("sha256").update(JSON.stringify([room, p.thread ?? "", p.text])).digest("base64url");
|
|
14956
14789
|
if (p.clientId !== undefined) {
|
|
14957
14790
|
const earlier = this.#store.chatSend(scope, p.clientId);
|
|
14958
14791
|
if (earlier) {
|
|
@@ -15349,7 +15182,7 @@ var init_chat = __esm(() => {
|
|
|
15349
15182
|
});
|
|
15350
15183
|
|
|
15351
15184
|
// packages/daemon/dist/log.js
|
|
15352
|
-
import { closeSync as closeSync5, constants as constants5, lstatSync as lstatSync3, renameSync as renameSync6, writeSync as
|
|
15185
|
+
import { closeSync as closeSync5, constants as constants5, lstatSync as lstatSync3, renameSync as renameSync6, writeSync as writeSync3 } from "fs";
|
|
15353
15186
|
function formatLine(level, event, fields = {}) {
|
|
15354
15187
|
const record = { t: new Date().toISOString(), level, event };
|
|
15355
15188
|
for (const [name, value] of Object.entries(fields))
|
|
@@ -15378,7 +15211,7 @@ function createLogger(options) {
|
|
|
15378
15211
|
}
|
|
15379
15212
|
const fd = openNoFollow(file, constants5.O_WRONLY | constants5.O_APPEND | constants5.O_CREAT, 384);
|
|
15380
15213
|
try {
|
|
15381
|
-
|
|
15214
|
+
writeSync3(fd, line);
|
|
15382
15215
|
} finally {
|
|
15383
15216
|
closeSync5(fd);
|
|
15384
15217
|
}
|
|
@@ -15445,9 +15278,9 @@ var init_log = __esm(() => {
|
|
|
15445
15278
|
});
|
|
15446
15279
|
|
|
15447
15280
|
// packages/daemon/dist/keyring.js
|
|
15448
|
-
import { createHash as
|
|
15281
|
+
import { createHash as createHash8, generateKeyPairSync as generateKeyPairSync3 } from "crypto";
|
|
15449
15282
|
function rotationOffset(machine) {
|
|
15450
|
-
return
|
|
15283
|
+
return createHash8("sha256").update(machine).digest().readUInt32BE(0) % ROTATION_SPREAD_MS;
|
|
15451
15284
|
}
|
|
15452
15285
|
function rotationDue(now, rotatedAt, days, machine) {
|
|
15453
15286
|
return days > 0 && now >= rotatedAt + days * DAY_MS + rotationOffset(machine);
|
|
@@ -15666,7 +15499,7 @@ class Keyring {
|
|
|
15666
15499
|
if (kept.length >= MAX_RETAINED) {
|
|
15667
15500
|
throw new MessagingError("invalid_state", "too many retained encryption keys; wait until the oldest expires, then rotate again");
|
|
15668
15501
|
}
|
|
15669
|
-
const next = withEncKey(identity,
|
|
15502
|
+
const next = withEncKey(identity, generateKeyPairSync3("x25519").privateKey);
|
|
15670
15503
|
const pending = { identity: next, retained: [{ key: identity.encKey }, ...kept] };
|
|
15671
15504
|
store.save(pending);
|
|
15672
15505
|
this.#keys = pending;
|
|
@@ -15701,7 +15534,7 @@ var init_keyring = __esm(() => {
|
|
|
15701
15534
|
});
|
|
15702
15535
|
|
|
15703
15536
|
// packages/daemon/dist/directory.js
|
|
15704
|
-
function presencePayload(rows, label, seq,
|
|
15537
|
+
function presencePayload(rows, label, seq, policy) {
|
|
15705
15538
|
const { share } = policy;
|
|
15706
15539
|
const sessions = rows.slice(0, LIMITS.presenceSessions).map((s) => {
|
|
15707
15540
|
const live = s.unregisteredAt === undefined;
|
|
@@ -15723,8 +15556,7 @@ function presencePayload(rows, label, seq, extra, policy) {
|
|
|
15723
15556
|
seq,
|
|
15724
15557
|
label,
|
|
15725
15558
|
sessions: [],
|
|
15726
|
-
features: [...FEATURES
|
|
15727
|
-
...extra?.direct ? { direct: extra.direct } : {},
|
|
15559
|
+
features: [...FEATURES],
|
|
15728
15560
|
...policy.away === true ? { away: true } : {}
|
|
15729
15561
|
};
|
|
15730
15562
|
return { ...payload, sessions: fitPresence(sessions, jcsBytes(payload), LIMITS.presenceBytes) };
|
|
@@ -15898,10 +15730,9 @@ function signHop(args) {
|
|
|
15898
15730
|
now,
|
|
15899
15731
|
...last !== undefined ? { parent: last } : {},
|
|
15900
15732
|
...scope !== undefined ? { scope } : {},
|
|
15901
|
-
...maxDepth !== undefined ? { maxDepth } : {}
|
|
15902
|
-
...request.external !== undefined ? { external: { principal: request.external.principal } } : {}
|
|
15733
|
+
...maxDepth !== undefined ? { maxDepth } : {}
|
|
15903
15734
|
});
|
|
15904
|
-
const hop = { by: identity.machine, from, to: args.to, task: args.task
|
|
15735
|
+
const hop = { by: identity.machine, from, to: args.to, task: args.task };
|
|
15905
15736
|
return verifyChain([...args.parent ?? [], grant], { realm, now, hop });
|
|
15906
15737
|
}
|
|
15907
15738
|
function inboundChain(raw, ctx) {
|
|
@@ -15909,8 +15740,7 @@ function inboundChain(raw, ctx) {
|
|
|
15909
15740
|
by: ctx.sender.machine,
|
|
15910
15741
|
from: formatAddress(ctx.sender.person, ctx.fromSession, ctx.sender.label),
|
|
15911
15742
|
to: formatAddress(ctx.me.person, ctx.toSession, ctx.me.label),
|
|
15912
|
-
task: ctx.task
|
|
15913
|
-
...ctx.external !== undefined ? { external: ctx.external.principal } : {}
|
|
15743
|
+
task: ctx.task
|
|
15914
15744
|
};
|
|
15915
15745
|
return verifyChain(raw, { realm: ctx.realm, now: ctx.now, hop });
|
|
15916
15746
|
}
|
|
@@ -15952,14 +15782,16 @@ class RosterIdentity {
|
|
|
15952
15782
|
#machine;
|
|
15953
15783
|
#catchUp;
|
|
15954
15784
|
#now;
|
|
15955
|
-
#listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
|
|
15785
|
+
#listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
|
|
15956
15786
|
#me;
|
|
15787
|
+
#length;
|
|
15957
15788
|
constructor(options) {
|
|
15958
15789
|
this.roster = options.roster;
|
|
15959
15790
|
this.#machine = options.machine;
|
|
15960
15791
|
this.#catchUp = options.catchUp;
|
|
15961
15792
|
this.#now = options.now;
|
|
15962
15793
|
this.#me = this.#selfRecord();
|
|
15794
|
+
this.#length = options.roster.entries().length;
|
|
15963
15795
|
}
|
|
15964
15796
|
get realm() {
|
|
15965
15797
|
return this.roster.realm;
|
|
@@ -15990,6 +15822,31 @@ class RosterIdentity {
|
|
|
15990
15822
|
successor(_device) {
|
|
15991
15823
|
return;
|
|
15992
15824
|
}
|
|
15825
|
+
deviceChanges(person) {
|
|
15826
|
+
const labels = new Map;
|
|
15827
|
+
const out = [];
|
|
15828
|
+
for (const entry of this.roster.entries()) {
|
|
15829
|
+
const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
|
|
15830
|
+
if (member) {
|
|
15831
|
+
labels.set(member.machine, member.label);
|
|
15832
|
+
if (member.person === person && entry.kind !== "genesis")
|
|
15833
|
+
out.push({ seq: entry.seq, change: "added", device: member.machine, label: member.label, vouch: "admin", by: entry.by, at: entry.ts });
|
|
15834
|
+
} else if (entry.kind === "member.remove") {
|
|
15835
|
+
const owner = this.#ownerAt(entry.body.machine);
|
|
15836
|
+
if (owner === person)
|
|
15837
|
+
out.push({ seq: entry.seq, change: "removed", device: entry.body.machine, label: labels.get(entry.body.machine) ?? "machine", vouch: "admin", by: entry.by, at: entry.ts });
|
|
15838
|
+
}
|
|
15839
|
+
}
|
|
15840
|
+
return out;
|
|
15841
|
+
}
|
|
15842
|
+
#ownerAt(machine) {
|
|
15843
|
+
for (const entry of this.roster.entries()) {
|
|
15844
|
+
const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
|
|
15845
|
+
if (member?.machine === machine)
|
|
15846
|
+
return member.person;
|
|
15847
|
+
}
|
|
15848
|
+
return;
|
|
15849
|
+
}
|
|
15993
15850
|
async devices(person, opts = {}) {
|
|
15994
15851
|
if (opts.atLeast !== undefined && this.relate(opts.atLeast).relation === "ahead") {
|
|
15995
15852
|
throw new MessagingError("roster_conflict", "the roster has not reached that anchor");
|
|
@@ -16033,6 +15890,20 @@ class RosterIdentity {
|
|
|
16033
15890
|
}
|
|
16034
15891
|
for (const listener of [...this.#listeners["devices-changed"]])
|
|
16035
15892
|
listener();
|
|
15893
|
+
const entries = this.roster.entries();
|
|
15894
|
+
if (entries.length !== this.#length) {
|
|
15895
|
+
const persons = new Set;
|
|
15896
|
+
for (const entry of entries.slice(this.#length)) {
|
|
15897
|
+
const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : undefined;
|
|
15898
|
+
const person = member?.person ?? (entry.kind === "member.remove" ? this.#ownerAt(entry.body.machine) : undefined);
|
|
15899
|
+
if (person !== undefined)
|
|
15900
|
+
persons.add(person);
|
|
15901
|
+
}
|
|
15902
|
+
this.#length = entries.length;
|
|
15903
|
+
if (persons.size > 0)
|
|
15904
|
+
for (const listener of [...this.#listeners["device-log"]])
|
|
15905
|
+
listener([...persons].sort());
|
|
15906
|
+
}
|
|
16036
15907
|
}
|
|
16037
15908
|
#selfRecord() {
|
|
16038
15909
|
const me = this.roster.member(this.#machine);
|
|
@@ -16134,7 +16005,6 @@ class DeviceLogin {
|
|
|
16134
16005
|
const pending = {
|
|
16135
16006
|
state: "pending",
|
|
16136
16007
|
purpose: args.purpose,
|
|
16137
|
-
verificationUriComplete: code.verificationUriComplete,
|
|
16138
16008
|
verificationUri: code.verificationUri,
|
|
16139
16009
|
userCode: code.userCode,
|
|
16140
16010
|
fingerprint: jellyKeyFingerprint(device),
|
|
@@ -16239,7 +16109,7 @@ var init_device_login = __esm(() => {
|
|
|
16239
16109
|
});
|
|
16240
16110
|
|
|
16241
16111
|
// packages/daemon/dist/realm/directory-identity.js
|
|
16242
|
-
import { randomBytes as
|
|
16112
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
16243
16113
|
|
|
16244
16114
|
class DirectoryIdentity {
|
|
16245
16115
|
kind = "directory";
|
|
@@ -16248,16 +16118,18 @@ class DirectoryIdentity {
|
|
|
16248
16118
|
#options;
|
|
16249
16119
|
#cache;
|
|
16250
16120
|
#keyring;
|
|
16251
|
-
#listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
|
|
16121
|
+
#listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
|
|
16252
16122
|
#head;
|
|
16253
16123
|
#persons = new Map;
|
|
16254
16124
|
#index = new Map;
|
|
16255
16125
|
#own;
|
|
16256
16126
|
#relations = new Map;
|
|
16257
16127
|
#watching = new Set;
|
|
16128
|
+
#watchRead = Promise.resolve();
|
|
16258
16129
|
#alarm;
|
|
16259
16130
|
#selfRecord;
|
|
16260
16131
|
#monitoredAt = Number.NEGATIVE_INFINITY;
|
|
16132
|
+
#bindAccepted = false;
|
|
16261
16133
|
#pendingLink;
|
|
16262
16134
|
constructor(options) {
|
|
16263
16135
|
this.#options = options;
|
|
@@ -16317,7 +16189,10 @@ class DirectoryIdentity {
|
|
|
16317
16189
|
const person = this.#own.person;
|
|
16318
16190
|
if (!person)
|
|
16319
16191
|
return false;
|
|
16320
|
-
const
|
|
16192
|
+
const held = this.#persons.get(person)?.state;
|
|
16193
|
+
if (held?.erased)
|
|
16194
|
+
return true;
|
|
16195
|
+
const record = held?.log.device(this.#machine());
|
|
16321
16196
|
return record !== undefined && !record.current;
|
|
16322
16197
|
}
|
|
16323
16198
|
persons() {
|
|
@@ -16338,6 +16213,12 @@ class DirectoryIdentity {
|
|
|
16338
16213
|
}
|
|
16339
16214
|
return;
|
|
16340
16215
|
}
|
|
16216
|
+
deviceChanges(person) {
|
|
16217
|
+
const held = isHosted(person) ? this.#persons.get(person) : undefined;
|
|
16218
|
+
if (!held || held.state.erased)
|
|
16219
|
+
return [];
|
|
16220
|
+
return deviceChangesOf(held.state.log);
|
|
16221
|
+
}
|
|
16341
16222
|
async devices(person, opts = {}) {
|
|
16342
16223
|
if (!isHosted(person))
|
|
16343
16224
|
return { person, devices: [], anchor: this.anchor(), fetchedAt: this.#options.now() };
|
|
@@ -16432,15 +16313,23 @@ class DirectoryIdentity {
|
|
|
16432
16313
|
}
|
|
16433
16314
|
}
|
|
16434
16315
|
watch(persons) {
|
|
16435
|
-
|
|
16316
|
+
const added = [];
|
|
16436
16317
|
for (const person of persons) {
|
|
16437
16318
|
if (!isHosted(person) || this.#watching.has(person) || this.#watching.size >= LIMITS.watchPersons)
|
|
16438
16319
|
continue;
|
|
16439
16320
|
this.#watching.add(person);
|
|
16440
|
-
|
|
16321
|
+
added.push(person);
|
|
16441
16322
|
}
|
|
16442
|
-
if (
|
|
16443
|
-
|
|
16323
|
+
if (added.length === 0)
|
|
16324
|
+
return this.#watchRead;
|
|
16325
|
+
this.#watchRead = this.#call((link) => link.watch([...this.#watching])).then(async () => {
|
|
16326
|
+
const held = added.filter((person) => this.#persons.has(person));
|
|
16327
|
+
if (held.length === 0)
|
|
16328
|
+
return;
|
|
16329
|
+
await this.#latest();
|
|
16330
|
+
await this.#fetch(held);
|
|
16331
|
+
}).catch(() => {});
|
|
16332
|
+
return this.#watchRead;
|
|
16444
16333
|
}
|
|
16445
16334
|
on(event, listener) {
|
|
16446
16335
|
const listeners = this.#listeners[event];
|
|
@@ -16451,6 +16340,10 @@ class DirectoryIdentity {
|
|
|
16451
16340
|
listeners.splice(at, 1);
|
|
16452
16341
|
};
|
|
16453
16342
|
}
|
|
16343
|
+
linkPerson() {
|
|
16344
|
+
const binding = this.binding();
|
|
16345
|
+
return binding.state !== "none" || this.#bindAccepted ? binding.person : undefined;
|
|
16346
|
+
}
|
|
16454
16347
|
alarm() {
|
|
16455
16348
|
return this.#alarm;
|
|
16456
16349
|
}
|
|
@@ -16460,6 +16353,8 @@ class DirectoryIdentity {
|
|
|
16460
16353
|
binding() {
|
|
16461
16354
|
const person = this.#own.person;
|
|
16462
16355
|
const record = person ? this.#persons.get(person)?.state.log.device(this.#machine()) : undefined;
|
|
16356
|
+
if (person && this.#persons.get(person)?.state.erased)
|
|
16357
|
+
return { state: "removed", person };
|
|
16463
16358
|
if (!person || !record)
|
|
16464
16359
|
return { state: "none", ...person ? { person } : {} };
|
|
16465
16360
|
if (!record.current)
|
|
@@ -16481,12 +16376,21 @@ class DirectoryIdentity {
|
|
|
16481
16376
|
throw new MessagingError("binding_invalid", "this device was removed from its account; it cannot be bound again");
|
|
16482
16377
|
const done = await this.#extendLog(async () => {
|
|
16483
16378
|
await this.#latest();
|
|
16484
|
-
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16379
|
+
let current;
|
|
16380
|
+
let head;
|
|
16381
|
+
if (this.#own.person === person && this.binding().state !== "none") {
|
|
16382
|
+
await this.#fetch([person]);
|
|
16383
|
+
const log = this.#persons.get(person)?.state.log;
|
|
16384
|
+
current = log?.device(identity.machine);
|
|
16385
|
+
if (current && !current.current)
|
|
16386
|
+
throw new MessagingError("binding_invalid", "this device was removed from its account; it cannot be bound again");
|
|
16387
|
+
head = log?.head();
|
|
16388
|
+
} else {
|
|
16389
|
+
const verified = await this.#headOf(person);
|
|
16390
|
+
if (verified.state === "erased")
|
|
16391
|
+
throw new MessagingError("binding_invalid", "the account was erased");
|
|
16392
|
+
head = verified.state === "live" ? verified.log : undefined;
|
|
16393
|
+
}
|
|
16490
16394
|
const entry = signLogEntry(identity, {
|
|
16491
16395
|
v: PROTOCOL,
|
|
16492
16396
|
realm: this.realm,
|
|
@@ -16500,13 +16404,29 @@ class DirectoryIdentity {
|
|
|
16500
16404
|
authority: { kind: "jelly", token }
|
|
16501
16405
|
});
|
|
16502
16406
|
this.#signed(entry, person);
|
|
16503
|
-
|
|
16407
|
+
if (current)
|
|
16408
|
+
return await this.#call((link) => link.append(entry));
|
|
16409
|
+
const bound = await this.#call((link) => link.bind(entry));
|
|
16410
|
+
this.#bindAccepted = true;
|
|
16411
|
+
return bound;
|
|
16504
16412
|
});
|
|
16505
16413
|
await this.#adoptAnnounced(done.sth);
|
|
16506
16414
|
await this.#fetch([person]);
|
|
16507
16415
|
await this.publishKeys();
|
|
16508
16416
|
return { person, seq: done.seq };
|
|
16509
16417
|
}
|
|
16418
|
+
async awaitSelf() {
|
|
16419
|
+
const person = this.#own.person;
|
|
16420
|
+
if (!person)
|
|
16421
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
16422
|
+
for (let tries = 0;this.self() === undefined || !this.#index.has(this.#machine()); tries++) {
|
|
16423
|
+
if (tries >= LOG_RETRIES)
|
|
16424
|
+
throw unavailable("the directory has not committed this device's binding and keys yet");
|
|
16425
|
+
await new Promise((resolve) => setTimeout(resolve, LOG_RETRY_MS));
|
|
16426
|
+
await this.#latest();
|
|
16427
|
+
await this.#fetch([person]);
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16510
16430
|
async publishKeys() {
|
|
16511
16431
|
if (this.#alarm?.kind === "restored")
|
|
16512
16432
|
throw new MessagingError("invalid_state", "this device runs from an older copy of its home; it publishes no keys");
|
|
@@ -16557,11 +16477,50 @@ class DirectoryIdentity {
|
|
|
16557
16477
|
await this.#fetch([person]);
|
|
16558
16478
|
return { seq: done.seq };
|
|
16559
16479
|
}
|
|
16480
|
+
async erase(token) {
|
|
16481
|
+
const identity = this.#options.identity();
|
|
16482
|
+
const person = this.#own.person;
|
|
16483
|
+
if (!person || this.binding().state === "none" || this.removed())
|
|
16484
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
16485
|
+
if (this.binding().record?.vouch !== "jelly")
|
|
16486
|
+
throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
|
|
16487
|
+
const entries = await this.#extendLog(async () => {
|
|
16488
|
+
await this.#latest();
|
|
16489
|
+
await this.#fetch([person]);
|
|
16490
|
+
const log = this.#persons.get(person).state.log;
|
|
16491
|
+
let head = log.head();
|
|
16492
|
+
const out = [];
|
|
16493
|
+
const devices = log.devices().sort((a, b) => Number(a.device === identity.machine) - Number(b.device === identity.machine));
|
|
16494
|
+
for (const device of devices) {
|
|
16495
|
+
const self = device.device === identity.machine;
|
|
16496
|
+
const entry = signLogEntry(identity, {
|
|
16497
|
+
v: PROTOCOL,
|
|
16498
|
+
realm: this.realm,
|
|
16499
|
+
person,
|
|
16500
|
+
seq: head.seq + 1,
|
|
16501
|
+
prev: head.hash,
|
|
16502
|
+
ts: this.#options.now(),
|
|
16503
|
+
op: "unbind",
|
|
16504
|
+
device: device.device,
|
|
16505
|
+
reason: self ? "self" : "removed",
|
|
16506
|
+
authority: self ? { kind: "self" } : { kind: "jelly", token }
|
|
16507
|
+
});
|
|
16508
|
+
out.push(entry);
|
|
16509
|
+
head = { seq: entry.seq, hash: logEntryHash(entry) };
|
|
16510
|
+
}
|
|
16511
|
+
for (const entry of out)
|
|
16512
|
+
this.#signed(entry, person);
|
|
16513
|
+
await this.#call((link) => link.erase(out, token));
|
|
16514
|
+
return out;
|
|
16515
|
+
});
|
|
16516
|
+
await this.#latest().then(() => this.#fetch([person])).catch(() => {});
|
|
16517
|
+
return { person, devices: entries.length, seq: entries.at(-1).seq };
|
|
16518
|
+
}
|
|
16560
16519
|
createLink(args) {
|
|
16561
16520
|
if (this.#own.person !== undefined) {
|
|
16562
16521
|
throw new MessagingError("invalid_state", this.removed() ? "this device was removed from its account; it cannot be bound again" : "this device is already bound");
|
|
16563
16522
|
}
|
|
16564
|
-
const link = createLinkRequest(this.#options.identity(), { realm: this.realm, label: args.label, kind: args.kind, nonce:
|
|
16523
|
+
const link = createLinkRequest(this.#options.identity(), { realm: this.realm, label: args.label, kind: args.kind, nonce: randomBytes7(16), ts: this.#options.now() });
|
|
16565
16524
|
this.#pendingLink = link;
|
|
16566
16525
|
return { link: encodeLink(link), sas: linkSas(link), expires: link.ts + LIMITS.linkTtlMs };
|
|
16567
16526
|
}
|
|
@@ -16755,8 +16714,9 @@ class DirectoryIdentity {
|
|
|
16755
16714
|
async#fetch(persons) {
|
|
16756
16715
|
if (!this.#head)
|
|
16757
16716
|
await this.#latest();
|
|
16717
|
+
const basis = new Map(persons.map((person) => [person, this.#persons.get(person)]));
|
|
16758
16718
|
const ask = persons.map((person) => {
|
|
16759
|
-
const held =
|
|
16719
|
+
const held = basis.get(person);
|
|
16760
16720
|
const length = held?.state.erased ? 0 : held?.state.log.length ?? 0;
|
|
16761
16721
|
return length > 0 ? { person, knownSeq: length - 1 } : { person };
|
|
16762
16722
|
});
|
|
@@ -16775,32 +16735,84 @@ class DirectoryIdentity {
|
|
|
16775
16735
|
throw new MessagingError("directory_inconsistent", "the directory answered at another tree head");
|
|
16776
16736
|
const now = this.#options.now();
|
|
16777
16737
|
const views = new Map(answer.persons.map((view) => [view.person, view]));
|
|
16738
|
+
const grew = [];
|
|
16778
16739
|
for (const person of persons) {
|
|
16779
16740
|
const view = views.get(person);
|
|
16780
16741
|
if (!view)
|
|
16781
16742
|
continue;
|
|
16782
|
-
const state = await this.#verify(view, head);
|
|
16743
|
+
const state = await this.#verify(view, head, basis.get(person)?.state);
|
|
16744
|
+
const current = this.#persons.get(person);
|
|
16745
|
+
if (current && current !== basis.get(person)) {
|
|
16746
|
+
this.#succession(current.size > head.sth.size ? state : current.state, current.size > head.sth.size ? current.state : state);
|
|
16747
|
+
if (current.size > head.sth.size)
|
|
16748
|
+
continue;
|
|
16749
|
+
}
|
|
16750
|
+
const before = current?.state;
|
|
16783
16751
|
this.#persons.set(person, { state, size: head.sth.size, fetchedAt: now });
|
|
16784
16752
|
this.#cache.putPerson({ person, entries: [...state.log.entries()], keys: [...state.keys.values()], erased: state.erased, size: head.sth.size, fetchedAt: now });
|
|
16753
|
+
if (state.log.length !== (before?.log.length ?? 0) || state.erased !== (before?.erased ?? false))
|
|
16754
|
+
grew.push(person);
|
|
16785
16755
|
}
|
|
16786
16756
|
this.#changed();
|
|
16757
|
+
if (grew.length > 0)
|
|
16758
|
+
for (const listener of [...this.#listeners["device-log"]])
|
|
16759
|
+
listener(grew);
|
|
16760
|
+
const unwatched = persons.filter((person) => views.has(person) && !this.#watching.has(person));
|
|
16761
|
+
if (unwatched.length > 0)
|
|
16762
|
+
this.watch(unwatched);
|
|
16763
|
+
}
|
|
16764
|
+
async#headOf(person) {
|
|
16765
|
+
if (!this.#head)
|
|
16766
|
+
await this.#latest();
|
|
16767
|
+
let head = this.#head;
|
|
16768
|
+
let answer;
|
|
16769
|
+
try {
|
|
16770
|
+
answer = await this.#call((link) => link.heads([person], head.sth.size));
|
|
16771
|
+
} catch (error) {
|
|
16772
|
+
if (!(error instanceof MessagingError) || error.code !== "invalid_request")
|
|
16773
|
+
throw error;
|
|
16774
|
+
await this.#latest();
|
|
16775
|
+
head = this.#head;
|
|
16776
|
+
answer = await this.#call((link) => link.heads([person], head.sth.size));
|
|
16777
|
+
}
|
|
16778
|
+
if (answer.sth.size !== head.sth.size || answer.sth.root !== head.sth.root)
|
|
16779
|
+
throw new MessagingError("directory_inconsistent", "the directory answered at another tree head");
|
|
16780
|
+
const view = answer.persons.find((p) => p.person === person);
|
|
16781
|
+
if (!view)
|
|
16782
|
+
throw new MessagingError("directory_inconsistent", "the directory left out the person asked");
|
|
16783
|
+
try {
|
|
16784
|
+
return verifyPersonHead(view, { sth: head.sth, head: head.head });
|
|
16785
|
+
} catch (error) {
|
|
16786
|
+
this.#inconsistent(error);
|
|
16787
|
+
throw error;
|
|
16788
|
+
}
|
|
16787
16789
|
}
|
|
16788
|
-
async#verify(view, head) {
|
|
16789
|
-
const known = this.#persons.get(view.person)?.state;
|
|
16790
|
+
async#verify(view, head, known) {
|
|
16790
16791
|
const tokens = view.log.flatMap((entry) => entry.authority.kind === "jelly" ? [entry.authority.token] : []);
|
|
16791
16792
|
const keys = await this.#keyring.prepare(tokens, this.#options.now());
|
|
16792
16793
|
this.#saveArchive();
|
|
16793
16794
|
try {
|
|
16794
16795
|
return verifyPersonView(view, { ...this.#context(view.person, keys), sth: head.sth, head: head.head, ...known && !known.erased ? { known } : {} });
|
|
16795
16796
|
} catch (error) {
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16797
|
+
this.#inconsistent(error);
|
|
16798
|
+
throw error;
|
|
16799
|
+
}
|
|
16800
|
+
}
|
|
16801
|
+
#succession(earlier, later) {
|
|
16802
|
+
try {
|
|
16803
|
+
checkPersonSuccession(earlier, later);
|
|
16804
|
+
} catch (error) {
|
|
16805
|
+
this.#inconsistent(error);
|
|
16801
16806
|
throw error;
|
|
16802
16807
|
}
|
|
16803
16808
|
}
|
|
16809
|
+
#inconsistent(error) {
|
|
16810
|
+
if (error instanceof DirectoryInconsistency) {
|
|
16811
|
+
this.#raise({ kind: "directory", reason: error.message, at: this.#options.now(), evidence: error.evidence });
|
|
16812
|
+
} else if (error instanceof MessagingError && error.code === "directory_inconsistent") {
|
|
16813
|
+
this.#raise({ kind: "directory", reason: error.message, at: this.#options.now() });
|
|
16814
|
+
}
|
|
16815
|
+
}
|
|
16804
16816
|
#saveArchive() {
|
|
16805
16817
|
this.#cache.setArchive(this.#keyring.archive());
|
|
16806
16818
|
}
|
|
@@ -16905,6 +16917,56 @@ ${this.#index.get(me.machine)?.enc ?? ""}`;
|
|
|
16905
16917
|
return mine === theirs ? "same" : "other";
|
|
16906
16918
|
}
|
|
16907
16919
|
}
|
|
16920
|
+
function deviceChangesOf(log) {
|
|
16921
|
+
const vouch = new Map;
|
|
16922
|
+
const record = new Map;
|
|
16923
|
+
const out = [];
|
|
16924
|
+
for (const entry of log.entries()) {
|
|
16925
|
+
const token = log.token(entry.seq);
|
|
16926
|
+
const via = token === undefined ? undefined : token.claims.hst !== undefined ? "host" : "browser";
|
|
16927
|
+
if (entry.op === "bind") {
|
|
16928
|
+
const v = entry.authority.kind === "jelly" ? "jelly" : "device";
|
|
16929
|
+
vouch.set(entry.device, v);
|
|
16930
|
+
record.set(entry.device, { label: entry.label, kind: entry.kind, host: via === "host" });
|
|
16931
|
+
out.push({
|
|
16932
|
+
seq: entry.seq,
|
|
16933
|
+
change: "added",
|
|
16934
|
+
device: entry.device,
|
|
16935
|
+
label: entry.label,
|
|
16936
|
+
kind: entry.kind,
|
|
16937
|
+
vouch: v,
|
|
16938
|
+
...via ? { via } : {},
|
|
16939
|
+
...entry.authority.kind === "device" ? { approver: entry.authority.approver } : {},
|
|
16940
|
+
by: entry.by,
|
|
16941
|
+
at: entry.ts
|
|
16942
|
+
});
|
|
16943
|
+
} else if (entry.op === "attest") {
|
|
16944
|
+
const known = record.get(entry.device);
|
|
16945
|
+
const next = { label: entry.label ?? known?.label ?? "device", kind: entry.kind ?? known?.kind ?? "other", host: via === "host" };
|
|
16946
|
+
record.set(entry.device, next);
|
|
16947
|
+
const raised = vouch.get(entry.device) !== "jelly";
|
|
16948
|
+
vouch.set(entry.device, "jelly");
|
|
16949
|
+
if (raised)
|
|
16950
|
+
out.push({ seq: entry.seq, change: "vouched", device: entry.device, label: next.label, kind: next.kind, vouch: "jelly", ...via ? { via } : {}, by: entry.by, at: entry.ts });
|
|
16951
|
+
} else {
|
|
16952
|
+
const known = record.get(entry.device);
|
|
16953
|
+
const v = vouch.get(entry.device) ?? "device";
|
|
16954
|
+
out.push({
|
|
16955
|
+
seq: entry.seq,
|
|
16956
|
+
change: entry.reason === "replaced" ? "replaced" : "removed",
|
|
16957
|
+
device: entry.device,
|
|
16958
|
+
label: known?.label ?? "device",
|
|
16959
|
+
...known ? { kind: known.kind } : {},
|
|
16960
|
+
vouch: v,
|
|
16961
|
+
...v === "jelly" && known ? { via: known.host ? "host" : "browser" } : {},
|
|
16962
|
+
by: entry.by,
|
|
16963
|
+
reason: entry.reason,
|
|
16964
|
+
at: entry.ts
|
|
16965
|
+
});
|
|
16966
|
+
}
|
|
16967
|
+
}
|
|
16968
|
+
return out;
|
|
16969
|
+
}
|
|
16908
16970
|
function orgOf(email, domains) {
|
|
16909
16971
|
if (email === undefined)
|
|
16910
16972
|
return;
|
|
@@ -16989,7 +17051,7 @@ class DirectoryHome {
|
|
|
16989
17051
|
url: this.#options.config.url,
|
|
16990
17052
|
realm: realm.realm,
|
|
16991
17053
|
identity: this.#options.identity,
|
|
16992
|
-
person: () => realm.
|
|
17054
|
+
person: () => realm.linkPerson(),
|
|
16993
17055
|
pushes: {
|
|
16994
17056
|
changed: (persons, sth) => void realm.changed(persons, sth).catch((error) => this.#failed("directory.changed", error)),
|
|
16995
17057
|
unbound: (entry, sth) => void realm.changed([entry.person], sth).catch((error) => this.#failed("directory.unbound", error)),
|
|
@@ -17123,7 +17185,6 @@ class DirectoryHome {
|
|
|
17123
17185
|
const started = await this.#login.start({ purpose, label, kind });
|
|
17124
17186
|
return {
|
|
17125
17187
|
purpose,
|
|
17126
|
-
verificationUriComplete: started.verificationUriComplete,
|
|
17127
17188
|
verificationUri: started.verificationUri,
|
|
17128
17189
|
userCode: started.userCode,
|
|
17129
17190
|
fingerprint: started.fingerprint,
|
|
@@ -17141,7 +17202,26 @@ class DirectoryHome {
|
|
|
17141
17202
|
this.#login.cancel();
|
|
17142
17203
|
return { ok: true };
|
|
17143
17204
|
}
|
|
17205
|
+
async signIn(purpose, args) {
|
|
17206
|
+
this.#removing = undefined;
|
|
17207
|
+
this.#binding = undefined;
|
|
17208
|
+
const started = await this.#login.start({ purpose, label: args.label, kind: args.kind });
|
|
17209
|
+
return {
|
|
17210
|
+
purpose,
|
|
17211
|
+
verificationUri: started.verificationUri,
|
|
17212
|
+
userCode: started.userCode,
|
|
17213
|
+
fingerprint: started.fingerprint,
|
|
17214
|
+
expires: started.expires,
|
|
17215
|
+
interval: started.interval
|
|
17216
|
+
};
|
|
17217
|
+
}
|
|
17144
17218
|
async#completeLogin(token, purpose) {
|
|
17219
|
+
if (purpose === "replace" || purpose === "erase") {
|
|
17220
|
+
const step = purpose === "replace" ? this.#options.replaceStep : this.#options.eraseStep;
|
|
17221
|
+
if (!step)
|
|
17222
|
+
throw new MessagingError("invalid_state", `this daemon does not ${purpose} through a sign-in`);
|
|
17223
|
+
return step(token);
|
|
17224
|
+
}
|
|
17145
17225
|
if (purpose === "remove") {
|
|
17146
17226
|
const removing = this.#removing;
|
|
17147
17227
|
if (!removing)
|
|
@@ -17207,6 +17287,28 @@ var init_directory_home = __esm(() => {
|
|
|
17207
17287
|
init_directory_identity();
|
|
17208
17288
|
});
|
|
17209
17289
|
|
|
17290
|
+
// packages/daemon/dist/realm/directory-cache.js
|
|
17291
|
+
function memoryDirectoryCache(from) {
|
|
17292
|
+
let head = from?.head();
|
|
17293
|
+
const persons = new Map((from?.persons() ?? []).map((p) => [p.person, p]));
|
|
17294
|
+
let archive = from?.archive() ?? [];
|
|
17295
|
+
let own = from?.own() ?? { keysSeq: -1, signed: [] };
|
|
17296
|
+
let alarm = from?.alarm();
|
|
17297
|
+
const copy = (value) => structuredClone(value);
|
|
17298
|
+
return {
|
|
17299
|
+
head: () => copy(head),
|
|
17300
|
+
setHead: (value) => void (head = copy(value)),
|
|
17301
|
+
persons: () => [...persons.values()].map(copy),
|
|
17302
|
+
putPerson: (value) => void persons.set(value.person, copy(value)),
|
|
17303
|
+
archive: () => copy(archive),
|
|
17304
|
+
setArchive: (keys) => void (archive = copy(keys)),
|
|
17305
|
+
own: () => copy(own),
|
|
17306
|
+
setOwn: (value) => void (own = copy(value)),
|
|
17307
|
+
alarm: () => copy(alarm),
|
|
17308
|
+
setAlarm: (value) => void (alarm = copy(value))
|
|
17309
|
+
};
|
|
17310
|
+
}
|
|
17311
|
+
|
|
17210
17312
|
// packages/daemon/dist/jelly.js
|
|
17211
17313
|
import { closeSync as closeSync6, constants as constants6, fstatSync as fstatSync3, readSync } from "fs";
|
|
17212
17314
|
function jellyLabel(text) {
|
|
@@ -17537,12 +17639,12 @@ var init_answer_preview = __esm(() => {
|
|
|
17537
17639
|
});
|
|
17538
17640
|
|
|
17539
17641
|
// packages/daemon/dist/snapshot.js
|
|
17540
|
-
import { createHash as
|
|
17642
|
+
import { createHash as createHash9 } from "crypto";
|
|
17541
17643
|
import { chmodSync as chmodSync4, chownSync as chownSync2, existsSync as existsSync6, lstatSync as lstatSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
17542
17644
|
import { join as join7 } from "path";
|
|
17543
17645
|
function sha256File(path) {
|
|
17544
17646
|
const data = readFileSync8(path);
|
|
17545
|
-
return { bytes: data.length, sha256:
|
|
17647
|
+
return { bytes: data.length, sha256: createHash9("sha256").update(data).digest("hex") };
|
|
17546
17648
|
}
|
|
17547
17649
|
|
|
17548
17650
|
class Snapshots {
|
|
@@ -17646,6 +17748,12 @@ class Notices {
|
|
|
17646
17748
|
alert(kind, at) {
|
|
17647
17749
|
this.#file({ id: `a_${kind.replace(/_/g, "")}${at}`, category: "alert", source: kind, at });
|
|
17648
17750
|
}
|
|
17751
|
+
device(seq, at) {
|
|
17752
|
+
this.#file({ id: `a_dev${seq}`, category: "alert", source: `device:${seq}`, at });
|
|
17753
|
+
}
|
|
17754
|
+
trust(grantId, person, until, at) {
|
|
17755
|
+
this.#file({ id: `a_trust${grantId}`, category: "alert", person, source: `trust:${until}`, at });
|
|
17756
|
+
}
|
|
17649
17757
|
#file(row) {
|
|
17650
17758
|
if (!this.#host.store.putNotice(row))
|
|
17651
17759
|
return;
|
|
@@ -17711,7 +17819,15 @@ class Notices {
|
|
|
17711
17819
|
const own = row.person === this.#host.me();
|
|
17712
17820
|
notice.from = profile ? { name: profile.name.slice(0, 200), ...profile.email !== undefined ? { email: profile.email.slice(0, 320) } : {}, org: profile.org } : { name: row.person, org: own ? "same" : "none" };
|
|
17713
17821
|
}
|
|
17714
|
-
if (row.category === "alert")
|
|
17822
|
+
if (row.category === "alert" && row.source.startsWith("device:")) {
|
|
17823
|
+
const device = this.#host.device?.(Number(row.source.slice("device:".length)));
|
|
17824
|
+
if (device)
|
|
17825
|
+
notice.device = device;
|
|
17826
|
+
notice.line = device ? deviceLine(device) : "A device of your account changed.";
|
|
17827
|
+
} else if (row.category === "alert" && row.source.startsWith("trust:")) {
|
|
17828
|
+
const until = Number(row.source.slice("trust:".length));
|
|
17829
|
+
notice.line = `Trust you gave this person ends on ${Number.isSafeInteger(until) ? new Date(until).toISOString().slice(0, 10) : "a date soon"}; renew it or let it lapse.`;
|
|
17830
|
+
} else if (row.category === "alert")
|
|
17715
17831
|
notice.line = ALERT_TEXT[row.source] ?? "An alarm was raised on this device.";
|
|
17716
17832
|
else if (row.category === "message" && lines) {
|
|
17717
17833
|
const line = this.#host.store.line(row.source);
|
|
@@ -17721,6 +17837,18 @@ class Notices {
|
|
|
17721
17837
|
return notice;
|
|
17722
17838
|
}
|
|
17723
17839
|
}
|
|
17840
|
+
function vouchWords2(d) {
|
|
17841
|
+
if (d.vouch === "admin")
|
|
17842
|
+
return "admitted by the team's admin";
|
|
17843
|
+
if (d.vouch === "device")
|
|
17844
|
+
return d.approver !== undefined ? `approved on '${d.approver}'` : "approved by link";
|
|
17845
|
+
return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID on a Jelly host";
|
|
17846
|
+
}
|
|
17847
|
+
function deviceLine(d) {
|
|
17848
|
+
const what = `'${d.label}'${d.kind !== undefined ? ` (${d.kind})` : ""}`;
|
|
17849
|
+
const line = d.change === "added" ? `New device on your account: ${what}, ${vouchWords2(d)}.` : d.change === "vouched" ? `${what} on your account is now vouched by Jelly ID: ${vouchWords2(d)}.` : d.change === "replaced" ? `'${d.replaces?.label ?? "a device"}' was replaced by ${what}, ${vouchWords2(d)}.` : `${what} was removed from your account.`;
|
|
17850
|
+
return line.length <= NOTICE_LINE_CHARS ? line : `${line.slice(0, NOTICE_LINE_CHARS - 1)}\u2026`;
|
|
17851
|
+
}
|
|
17724
17852
|
function firstLine(text) {
|
|
17725
17853
|
const first = cleanPeerText(text.split(/\r\n|\n|\r/, 1)[0] ?? "", { singleLine: true });
|
|
17726
17854
|
if (first.length <= NOTICE_LINE_CHARS)
|
|
@@ -18301,46 +18429,44 @@ var init_storage_errors = __esm(() => {
|
|
|
18301
18429
|
});
|
|
18302
18430
|
|
|
18303
18431
|
// packages/daemon/dist/store.js
|
|
18304
|
-
import { chmodSync as chmodSync5, closeSync as closeSync7, openSync as openSync3 } from "fs";
|
|
18432
|
+
import { chmodSync as chmodSync5, closeSync as closeSync7, existsSync as existsSync7, openSync as openSync3 } from "fs";
|
|
18305
18433
|
import { DatabaseSync } from "node:sqlite";
|
|
18306
|
-
function
|
|
18307
|
-
if (
|
|
18308
|
-
return
|
|
18309
|
-
|
|
18434
|
+
function checkStore(db, path) {
|
|
18435
|
+
if (db.prepare("SELECT 1 FROM sqlite_master LIMIT 1").get() === undefined)
|
|
18436
|
+
return true;
|
|
18437
|
+
const hasKv = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'kv'").get() !== undefined;
|
|
18438
|
+
const raw = hasKv ? kvGet(db, "generation") : undefined;
|
|
18439
|
+
if (raw === undefined) {
|
|
18440
|
+
throw new Error(`${path} was written before the store reset (it records no store generation), and this messagingd does not read it. ${RESET_HINT}`);
|
|
18441
|
+
}
|
|
18442
|
+
if (storeNumber(raw) !== STORE_GENERATION) {
|
|
18443
|
+
throw new Error(`${path} is of store generation ${raw.slice(0, 16)}; this messagingd reads generation ${STORE_GENERATION}. ${RESET_HINT}`);
|
|
18444
|
+
}
|
|
18445
|
+
const minReader = kvGet(db, "min_reader") ?? "";
|
|
18446
|
+
if ((storeNumber(minReader) ?? Number.POSITIVE_INFINITY) > STORE_SCHEMA) {
|
|
18447
|
+
throw new Error(`${path} needs a messagingd that reads store schema ${minReader.slice(0, 16) || "(unrecorded)"} or later; this one reads up to ${STORE_SCHEMA}. Run a newer messagingd`);
|
|
18448
|
+
}
|
|
18449
|
+
return false;
|
|
18450
|
+
}
|
|
18451
|
+
function checkStoreFile(path) {
|
|
18452
|
+
if (!existsSync7(path))
|
|
18453
|
+
return;
|
|
18454
|
+
const db = new DatabaseSync(path, { readOnly: true, timeout: 5000 });
|
|
18455
|
+
try {
|
|
18456
|
+
checkStore(db, path);
|
|
18457
|
+
} finally {
|
|
18458
|
+
db.close();
|
|
18459
|
+
}
|
|
18310
18460
|
}
|
|
18311
|
-
function
|
|
18461
|
+
function openStore(db, path) {
|
|
18312
18462
|
db.exec("BEGIN IMMEDIATE");
|
|
18313
18463
|
try {
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
const top = migrations.reduce((max, m) => Math.max(max, m.schema), STORE_SCHEMA);
|
|
18319
|
-
if (raw !== undefined && (stored === undefined || stored < STORE_MIN_READER)) {
|
|
18320
|
-
throw new Error(`messaging.db has schema ${raw.slice(0, 16)}, from before the first released store (schema ${STORE_MIN_READER}); move the Messaging home aside and set it up again`);
|
|
18321
|
-
}
|
|
18322
|
-
if (minReader !== undefined && minReader > top) {
|
|
18323
|
-
throw new Error(`messaging.db needs a messagingd that reads store schema ${minReader} or later; this one reads up to ${top}. Run a newer messagingd`);
|
|
18324
|
-
}
|
|
18325
|
-
db.exec(BASELINE);
|
|
18326
|
-
for (const migration of migrations) {
|
|
18327
|
-
if (migration.ddl) {
|
|
18328
|
-
if (/\b(DROP|RENAME|ALTER)\b|\bCREATE\s+UNIQUE\b/i.test(migration.ddl))
|
|
18329
|
-
throw new Error(`store migration ${migration.schema} is not additive`);
|
|
18330
|
-
db.exec(migration.ddl);
|
|
18331
|
-
}
|
|
18332
|
-
for (const { table, column, decl } of migration.columns ?? []) {
|
|
18333
|
-
if (!/^[a-z_][a-z0-9_]*$/.test(table) || !/^[a-z_][a-z0-9_]*$/.test(column) || !additiveColumn(decl)) {
|
|
18334
|
-
throw new Error(`store migration ${migration.schema} is not additive (${table}.${column})`);
|
|
18335
|
-
}
|
|
18336
|
-
const has = db.prepare(`SELECT name FROM pragma_table_info('${table}')`).all().some((c) => c.name === column);
|
|
18337
|
-
if (!has)
|
|
18338
|
-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
|
|
18339
|
-
}
|
|
18340
|
-
}
|
|
18341
|
-
kvSet(db, "schema", String(Math.max(stored ?? 0, top)));
|
|
18342
|
-
if (kvGet(db, "min_reader") === undefined)
|
|
18464
|
+
if (checkStore(db, path)) {
|
|
18465
|
+
db.exec(BASELINE);
|
|
18466
|
+
kvSet(db, "generation", String(STORE_GENERATION));
|
|
18467
|
+
kvSet(db, "schema", String(STORE_SCHEMA));
|
|
18343
18468
|
kvSet(db, "min_reader", String(STORE_MIN_READER));
|
|
18469
|
+
}
|
|
18344
18470
|
db.exec("COMMIT");
|
|
18345
18471
|
} catch (error) {
|
|
18346
18472
|
db.exec("ROLLBACK");
|
|
@@ -18395,7 +18521,7 @@ class Store {
|
|
|
18395
18521
|
this.#db = new DatabaseSync(path, { timeout: 5000 });
|
|
18396
18522
|
this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;");
|
|
18397
18523
|
try {
|
|
18398
|
-
|
|
18524
|
+
openStore(this.#db, path);
|
|
18399
18525
|
} catch (error) {
|
|
18400
18526
|
this.#db.close();
|
|
18401
18527
|
throw error;
|
|
@@ -18583,7 +18709,7 @@ class Store {
|
|
|
18583
18709
|
}
|
|
18584
18710
|
insertInbox(row) {
|
|
18585
18711
|
const { key, stage, ...item } = row.item;
|
|
18586
|
-
this.#run("INSERT INTO inbox (id, key, session, sender, item, message, stage, received_at, bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", item.id, key, row.session, row.sender, JSON.stringify(
|
|
18712
|
+
this.#run("INSERT INTO inbox (id, key, session, sender, item, message, stage, received_at, bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", item.id, key, row.session, row.sender, JSON.stringify(item), JSON.stringify(row.message), stage, item.receivedAt, item.bytes);
|
|
18587
18713
|
}
|
|
18588
18714
|
inbox(key, unreadOnly, limit) {
|
|
18589
18715
|
const sql = `SELECT * FROM inbox WHERE key = ?${unreadOnly ? " AND stage <> 'read'" : ""} ORDER BY received_at DESC, rowid DESC LIMIT ?`;
|
|
@@ -18984,7 +19110,7 @@ class Store {
|
|
|
18984
19110
|
const prefix = `${peer}@`;
|
|
18985
19111
|
const take = limit + 1;
|
|
18986
19112
|
const [inboxBound, inboxArgs] = bound("received_at", "id");
|
|
18987
|
-
const mail = this.#all(`SELECT * FROM inbox WHERE key = ? AND json_extract(item, '$.kind') = 'message' AND json_extract(item, '$.from.person') = ? AND json_extract(item, '$.from.
|
|
19113
|
+
const mail = this.#all(`SELECT * FROM inbox WHERE key = ? AND json_extract(item, '$.kind') = 'message' AND json_extract(item, '$.from.person') = ? AND json_extract(item, '$.from.agent') = ?${inboxBound} ORDER BY received_at ${order}, id ${order} LIMIT ?`, key, person, session, ...inboxArgs, take).map((r) => ({ source: "inbox", at: Number(r.received_at), id: String(r.id), inbox: inboxRow2(r) }));
|
|
18988
19114
|
const [sentBound, sentArgs] = bound("created_at", "id");
|
|
18989
19115
|
const sent = this.#all(`SELECT * FROM sent WHERE key = ? AND kind = 'message' AND substr(to_addr, 1, ?) = ?${sentBound} ORDER BY created_at ${order}, id ${order} LIMIT ?`, key, prefix.length, prefix, ...sentArgs, take).map((r) => ({ source: "sent", at: Number(r.created_at), id: String(r.id), sent: sentRow(r) }));
|
|
18990
19116
|
const [taskBound, taskArgs] = bound("created_at", "task_id");
|
|
@@ -19039,7 +19165,7 @@ class Store {
|
|
|
19039
19165
|
}
|
|
19040
19166
|
agentPeers(key, limit) {
|
|
19041
19167
|
return this.#all(`SELECT peer, SUM(unread) AS unread, MAX(at) AS at FROM (
|
|
19042
|
-
SELECT json_extract(item, '$.from.person') || '/' || json_extract(item, '$.from.
|
|
19168
|
+
SELECT json_extract(item, '$.from.person') || '/' || json_extract(item, '$.from.agent') AS peer, stage <> 'read' AS unread, received_at AS at
|
|
19043
19169
|
FROM inbox WHERE key = ? AND json_extract(item, '$.kind') = 'message'
|
|
19044
19170
|
UNION ALL
|
|
19045
19171
|
SELECT substr(to_addr, 1, instr(to_addr || '@', '@') - 1), 0, created_at FROM sent WHERE key = ? AND kind = 'message'
|
|
@@ -19066,7 +19192,7 @@ class Store {
|
|
|
19066
19192
|
ON CONFLICT (task_id, role) DO UPDATE SET key = excluded.key, session = excluded.session, peer = excluded.peer,
|
|
19067
19193
|
peer_machine = excluded.peer_machine, peer_address = excluded.peer_address, state = excluded.state,
|
|
19068
19194
|
context_id = excluded.context_id, task = excluded.task, updated_at = excluded.updated_at,
|
|
19069
|
-
request = COALESCE(excluded.request, tasks.request)`, r.task.id, r.role, r.key, row.session, JSON.stringify(
|
|
19195
|
+
request = COALESCE(excluded.request, tasks.request)`, r.task.id, r.role, r.key, row.session, JSON.stringify(r.peer), r.peer.device, r.peer.address, r.task.status.state, opt(r.task.contextId), JSON.stringify(r.task), r.createdAt, r.updatedAt, opt(row.request));
|
|
19070
19196
|
if (r.chain !== undefined) {
|
|
19071
19197
|
this.#run("INSERT OR IGNORE INTO delegations (task_id, role, grants, chain) VALUES (?, ?, ?, ?)", r.task.id, r.role, JSON.stringify(row.grants), JSON.stringify(r.chain));
|
|
19072
19198
|
}
|
|
@@ -19169,35 +19295,12 @@ function directoryRow(r) {
|
|
|
19169
19295
|
function inboxRow2(r) {
|
|
19170
19296
|
const item = json(r.item);
|
|
19171
19297
|
return {
|
|
19172
|
-
item: { ...item,
|
|
19298
|
+
item: { ...item, key: String(r.key), stage: String(r.stage) },
|
|
19173
19299
|
session: String(r.session),
|
|
19174
19300
|
sender: String(r.sender),
|
|
19175
|
-
message:
|
|
19176
|
-
};
|
|
19177
|
-
}
|
|
19178
|
-
function storedPeer(peer) {
|
|
19179
|
-
const { agent, device, ...rest } = peer;
|
|
19180
|
-
return { ...rest, ...agent !== undefined ? { session: agent } : {}, machine: device };
|
|
19181
|
-
}
|
|
19182
|
-
function peerOf(stored) {
|
|
19183
|
-
const { session, machine, ...rest } = stored;
|
|
19184
|
-
return { ...rest, ...session !== undefined ? { agent: session } : {}, device: machine };
|
|
19185
|
-
}
|
|
19186
|
-
function storedMessage(message) {
|
|
19187
|
-
const provenance = message.metadata?.[URI.provenance];
|
|
19188
|
-
return provenance === undefined ? message : { ...message, metadata: { ...message.metadata, [URI.provenance]: storedPeer(provenance) } };
|
|
19189
|
-
}
|
|
19190
|
-
function taskMessages(task, convert) {
|
|
19191
|
-
return {
|
|
19192
|
-
...task,
|
|
19193
|
-
status: task.status.message !== undefined ? { ...task.status, message: convert(task.status.message) } : task.status,
|
|
19194
|
-
...task.history !== undefined ? { history: task.history.map(convert) } : {}
|
|
19301
|
+
message: json(r.message)
|
|
19195
19302
|
};
|
|
19196
19303
|
}
|
|
19197
|
-
function messageOf2(message) {
|
|
19198
|
-
const provenance = message.metadata?.[URI.provenance];
|
|
19199
|
-
return provenance === undefined ? message : { ...message, metadata: { ...message.metadata, [URI.provenance]: peerOf(provenance) } };
|
|
19200
|
-
}
|
|
19201
19304
|
function outboxRow(r) {
|
|
19202
19305
|
return {
|
|
19203
19306
|
id: String(r.id),
|
|
@@ -19274,10 +19377,10 @@ function matchQuery(query) {
|
|
|
19274
19377
|
function taskRow2(r) {
|
|
19275
19378
|
const row = {
|
|
19276
19379
|
record: {
|
|
19277
|
-
task:
|
|
19380
|
+
task: json(r.task),
|
|
19278
19381
|
role: String(r.role),
|
|
19279
19382
|
key: String(r.key),
|
|
19280
|
-
peer:
|
|
19383
|
+
peer: json(r.peer),
|
|
19281
19384
|
createdAt: Number(r.created_at),
|
|
19282
19385
|
updatedAt: Number(r.updated_at)
|
|
19283
19386
|
},
|
|
@@ -19293,7 +19396,7 @@ function taskRow2(r) {
|
|
|
19293
19396
|
}
|
|
19294
19397
|
return row;
|
|
19295
19398
|
}
|
|
19296
|
-
var CHAT_COUNTER = "chat", STORE_SCHEMA =
|
|
19399
|
+
var CHAT_COUNTER = "chat", STORE_GENERATION = 2, STORE_SCHEMA = 1, STORE_MIN_READER = 1, BASELINE = `
|
|
19297
19400
|
CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL) STRICT;
|
|
19298
19401
|
CREATE TABLE IF NOT EXISTS counters (name TEXT PRIMARY KEY, value INTEGER NOT NULL) STRICT;
|
|
19299
19402
|
CREATE TABLE IF NOT EXISTS roster (seq INTEGER PRIMARY KEY, hash TEXT NOT NULL, entry TEXT NOT NULL) STRICT;
|
|
@@ -19399,7 +19502,8 @@ CREATE TABLE IF NOT EXISTS chat_sent (
|
|
|
19399
19502
|
person TEXT NOT NULL,
|
|
19400
19503
|
stage TEXT NOT NULL,
|
|
19401
19504
|
code TEXT,
|
|
19402
|
-
exp INTEGER NOT NULL
|
|
19505
|
+
exp INTEGER NOT NULL,
|
|
19506
|
+
held_at INTEGER
|
|
19403
19507
|
) STRICT;
|
|
19404
19508
|
CREATE INDEX IF NOT EXISTS chat_sent_by_line ON chat_sent (line, person);
|
|
19405
19509
|
CREATE INDEX IF NOT EXISTS chat_sent_pending ON chat_sent (exp) WHERE stage = 'sent';
|
|
@@ -19442,6 +19546,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
19442
19546
|
task TEXT NOT NULL,
|
|
19443
19547
|
created_at INTEGER NOT NULL,
|
|
19444
19548
|
updated_at INTEGER NOT NULL,
|
|
19549
|
+
request TEXT,
|
|
19445
19550
|
PRIMARY KEY (task_id, role)
|
|
19446
19551
|
) STRICT;
|
|
19447
19552
|
CREATE INDEX IF NOT EXISTS tasks_by_key ON tasks (key, updated_at DESC, task_id DESC, role DESC);
|
|
@@ -19470,37 +19575,20 @@ CREATE TABLE IF NOT EXISTS idempotency (
|
|
|
19470
19575
|
created_at INTEGER NOT NULL,
|
|
19471
19576
|
PRIMARY KEY (key, ikey)
|
|
19472
19577
|
) STRICT;
|
|
19473
|
-
|
|
19474
|
-
|
|
19475
|
-
|
|
19476
|
-
}, kvSet = (db, k, v) => {
|
|
19477
|
-
db.prepare("INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v").run(k, v);
|
|
19478
|
-
}, schemaNumber = (value) => value !== undefined && /^[1-9][0-9]{0,8}$/.test(value) ? Number(value) : undefined, TASKS = "tasks LEFT JOIN delegations USING (task_id, role)", text2 = (v) => v === null || v === undefined ? undefined : String(v), num = (v) => v === null || v === undefined ? undefined : Number(v), json = (v) => JSON.parse(String(v)), opt = (v) => v === undefined ? null : v, UNSPACED;
|
|
19479
|
-
var init_store2 = __esm(() => {
|
|
19480
|
-
init_dist();
|
|
19481
|
-
MIGRATIONS = [
|
|
19482
|
-
{ schema: 4, columns: [{ table: "tasks", column: "request", decl: "TEXT" }] },
|
|
19483
|
-
{
|
|
19484
|
-
schema: 5,
|
|
19485
|
-
ddl: `CREATE TABLE IF NOT EXISTS dir_persons (
|
|
19578
|
+
-- A directory realm's cache of verified persons (spec \xA715.9); its tree head, Jelly key archive, own record and alarm
|
|
19579
|
+
-- are kv rows (dir.*).
|
|
19580
|
+
CREATE TABLE IF NOT EXISTS dir_persons (
|
|
19486
19581
|
person TEXT PRIMARY KEY,
|
|
19487
19582
|
entries TEXT NOT NULL,
|
|
19488
19583
|
keys TEXT NOT NULL,
|
|
19489
19584
|
erased INTEGER NOT NULL DEFAULT 0,
|
|
19490
19585
|
size INTEGER NOT NULL,
|
|
19491
19586
|
fetched_at INTEGER NOT NULL
|
|
19492
|
-
) STRICT
|
|
19493
|
-
|
|
19494
|
-
{
|
|
19495
|
-
schema: 6,
|
|
19496
|
-
ddl: `
|
|
19587
|
+
) STRICT;
|
|
19588
|
+
-- This person's grants, one per subject, as signed; and the widening levels that lapsed with their signer (spec \xA76.9).
|
|
19497
19589
|
CREATE TABLE IF NOT EXISTS grants (subject TEXT PRIMARY KEY, grant TEXT NOT NULL) STRICT;
|
|
19498
19590
|
CREATE TABLE IF NOT EXISTS grant_reviews (subject TEXT PRIMARY KEY, id TEXT NOT NULL, levels TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
|
|
19499
|
-
|
|
19500
|
-
},
|
|
19501
|
-
{
|
|
19502
|
-
schema: 7,
|
|
19503
|
-
ddl: `
|
|
19591
|
+
-- Requests held for this person to answer (spec \xA76.9, "Holds"): the envelope as received and its opened payload.
|
|
19504
19592
|
CREATE TABLE IF NOT EXISTS held (
|
|
19505
19593
|
id TEXT PRIMARY KEY,
|
|
19506
19594
|
person TEXT NOT NULL,
|
|
@@ -19513,11 +19601,8 @@ CREATE TABLE IF NOT EXISTS held (
|
|
|
19513
19601
|
) STRICT;
|
|
19514
19602
|
CREATE INDEX IF NOT EXISTS held_person ON held (person);
|
|
19515
19603
|
CREATE INDEX IF NOT EXISTS held_exp ON held (exp);
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
{
|
|
19519
|
-
schema: 8,
|
|
19520
|
-
ddl: `
|
|
19604
|
+
-- Deployment "jelly" (spec \xA78.11): spent action-grant ids until their expiry, and the Jelly agent each session key
|
|
19605
|
+
-- was registered for. Notices (spec \xA78.2, "Notices") name their source (a line, a held request, an alarm).
|
|
19521
19606
|
CREATE TABLE IF NOT EXISTS act_jti (jti TEXT PRIMARY KEY, exp INTEGER NOT NULL) STRICT;
|
|
19522
19607
|
CREATE INDEX IF NOT EXISTS act_jti_exp ON act_jti (exp);
|
|
19523
19608
|
CREATE TABLE IF NOT EXISTS session_agents (key TEXT PRIMARY KEY, agt TEXT NOT NULL) STRICT;
|
|
@@ -19532,11 +19617,8 @@ CREATE TABLE IF NOT EXISTS notices (
|
|
|
19532
19617
|
acked_at INTEGER
|
|
19533
19618
|
) STRICT;
|
|
19534
19619
|
CREATE INDEX IF NOT EXISTS notices_at ON notices (at);
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
{
|
|
19538
|
-
schema: 9,
|
|
19539
|
-
ddl: `
|
|
19620
|
+
-- Contact cards and message requests (spec \xA76.9): the latest card each other person sent this one; message requests
|
|
19621
|
+
-- held for the person to accept, as received and opened; and the contacts this person's agents suggested.
|
|
19540
19622
|
CREATE TABLE IF NOT EXISTS contact_cards (person TEXT PRIMARY KEY, card TEXT NOT NULL, seq INTEGER NOT NULL, at INTEGER NOT NULL) STRICT;
|
|
19541
19623
|
CREATE TABLE IF NOT EXISTS contact_requests (
|
|
19542
19624
|
id TEXT PRIMARY KEY,
|
|
@@ -19549,10 +19631,14 @@ CREATE TABLE IF NOT EXISTS contact_requests (
|
|
|
19549
19631
|
) STRICT;
|
|
19550
19632
|
CREATE INDEX IF NOT EXISTS contact_requests_person ON contact_requests (person);
|
|
19551
19633
|
CREATE TABLE IF NOT EXISTS contact_suggestions (id TEXT PRIMARY KEY, session TEXT NOT NULL, email TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
|
|
19552
|
-
`,
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19634
|
+
`, kvGet = (db, k) => {
|
|
19635
|
+
const row = db.prepare("SELECT v FROM kv WHERE k = ?").get(k);
|
|
19636
|
+
return row?.v === undefined || row.v === null ? undefined : String(row.v);
|
|
19637
|
+
}, kvSet = (db, k, v) => {
|
|
19638
|
+
db.prepare("INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v").run(k, v);
|
|
19639
|
+
}, storeNumber = (value) => value !== undefined && /^[1-9][0-9]{0,8}$/.test(value) ? Number(value) : undefined, RESET_HINT = "Nothing was changed. To start over, stop messagingd, move the whole Messaging home aside (it keeps the old messages, and the messagingd that wrote it still opens it), then set the home up again: messaging login, or messaging team create or team join for a self-hosted team", TASKS = "tasks LEFT JOIN delegations USING (task_id, role)", text2 = (v) => v === null || v === undefined ? undefined : String(v), num = (v) => v === null || v === undefined ? undefined : Number(v), json = (v) => JSON.parse(String(v)), opt = (v) => v === undefined ? null : v, UNSPACED;
|
|
19640
|
+
var init_store2 = __esm(() => {
|
|
19641
|
+
init_dist();
|
|
19556
19642
|
UNSPACED = /([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}])/gu;
|
|
19557
19643
|
});
|
|
19558
19644
|
|
|
@@ -19945,6 +20031,16 @@ class Grants {
|
|
|
19945
20031
|
hasGrant(person) {
|
|
19946
20032
|
return this.#current(grantSubjectKey({ person })) !== undefined;
|
|
19947
20033
|
}
|
|
20034
|
+
expiringTrust(withinMs) {
|
|
20035
|
+
const now = this.#host.now();
|
|
20036
|
+
const current = this.#currentDevice();
|
|
20037
|
+
return this.#all().flatMap(({ grant }) => {
|
|
20038
|
+
const until = grant.trustedUntil;
|
|
20039
|
+
if (!("person" in grant.subject) || until === undefined || until <= now || until > now + withinMs || !current(grant.by))
|
|
20040
|
+
return [];
|
|
20041
|
+
return [{ id: grant.id, person: grant.subject.person, until }];
|
|
20042
|
+
});
|
|
20043
|
+
}
|
|
19948
20044
|
list(p) {
|
|
19949
20045
|
const only = p.subject === undefined ? undefined : this.#subjectKey(p.subject, false);
|
|
19950
20046
|
const grants = this.#all().filter(({ subject }) => only === undefined || subject === only).map(({ grant }) => this.#view(grant)).sort((a, b) => b.ts - a.ts || (a.id < b.id ? 1 : -1));
|
|
@@ -20019,6 +20115,25 @@ class Grants {
|
|
|
20019
20115
|
this.#host.log("grant.sync", { id: env.id, what: conv.what, merged });
|
|
20020
20116
|
this.sweep();
|
|
20021
20117
|
}
|
|
20118
|
+
resignFrom(predecessor) {
|
|
20119
|
+
const { identity } = this.#host.joined();
|
|
20120
|
+
const now = this.#host.now();
|
|
20121
|
+
const resigned = [];
|
|
20122
|
+
for (const { subject, grant } of this.#all()) {
|
|
20123
|
+
if (grant.by !== predecessor)
|
|
20124
|
+
continue;
|
|
20125
|
+
const copy = resignGrant(identity, grant, { now, keep: "all" });
|
|
20126
|
+
this.#store.putGrant(subject, copy);
|
|
20127
|
+
resigned.push(copy);
|
|
20128
|
+
if ("person" in copy.subject)
|
|
20129
|
+
this.#host.changed?.(copy.subject.person);
|
|
20130
|
+
}
|
|
20131
|
+
if (resigned.length > 0) {
|
|
20132
|
+
this.#sync("grant", resigned);
|
|
20133
|
+
this.#host.log("grant.resigned", { grants: resigned.length });
|
|
20134
|
+
}
|
|
20135
|
+
return resigned.length;
|
|
20136
|
+
}
|
|
20022
20137
|
sweep() {
|
|
20023
20138
|
const { identity, realm } = this.#host.joined();
|
|
20024
20139
|
if (realm.removed())
|
|
@@ -20814,6 +20929,7 @@ class Daemon {
|
|
|
20814
20929
|
const realm = this.#realm ?? this.#directory?.realm;
|
|
20815
20930
|
return realm ? await realm.profile(person) : undefined;
|
|
20816
20931
|
},
|
|
20932
|
+
device: (seq) => this.#noticeDevice(seq),
|
|
20817
20933
|
emit: (event) => this.#emit(event)
|
|
20818
20934
|
});
|
|
20819
20935
|
this.#keyring = new Keyring({
|
|
@@ -20845,6 +20961,7 @@ class Daemon {
|
|
|
20845
20961
|
static async start(options) {
|
|
20846
20962
|
const paths = homePaths(options.home, options.jellyHost?.run);
|
|
20847
20963
|
ensureHome(paths);
|
|
20964
|
+
checkStoreFile(paths.db);
|
|
20848
20965
|
const jelly = options.jellyHost;
|
|
20849
20966
|
if (jelly?.realm !== undefined && readConfig(paths.config).realm === undefined) {
|
|
20850
20967
|
options = { ...options, jellyHost: { ...jelly, realm: await pinRealm(jelly.realm, options.wellKnown) } };
|
|
@@ -20919,6 +21036,7 @@ class Daemon {
|
|
|
20919
21036
|
}
|
|
20920
21037
|
this.#log("info", "daemon.started", { pid: process.pid, version: VERSION, joined: identity !== undefined, store: identityStore?.kind, ...jelly ? { deployment: "jelly" } : {} });
|
|
20921
21038
|
this.#state = "running";
|
|
21039
|
+
this.#deviceNotices(true);
|
|
20922
21040
|
this.#startTransport();
|
|
20923
21041
|
this.#sweepTimer = setInterval(() => this.sweep(), this.#tuning.sweepMs);
|
|
20924
21042
|
this.#sweepTimer.unref();
|
|
@@ -20958,7 +21076,12 @@ class Daemon {
|
|
|
20958
21076
|
this.#alerts();
|
|
20959
21077
|
},
|
|
20960
21078
|
onDeliver: (raw) => this.#onDeliver(raw),
|
|
20961
|
-
state: (connected) => this.#onHostedState(connected)
|
|
21079
|
+
state: (connected) => this.#onHostedState(connected),
|
|
21080
|
+
replaceStep: (token) => this.#replaceWith(token),
|
|
21081
|
+
eraseStep: async (token) => {
|
|
21082
|
+
const done = await this.#eraseWith(token);
|
|
21083
|
+
return { person: done.person, device: this.#identity.machine, seq: done.seq };
|
|
21084
|
+
}
|
|
20962
21085
|
});
|
|
20963
21086
|
this.#directory = home;
|
|
20964
21087
|
const realm = home.realm;
|
|
@@ -20976,6 +21099,7 @@ class Daemon {
|
|
|
20976
21099
|
else
|
|
20977
21100
|
this.#presenceSoon();
|
|
20978
21101
|
});
|
|
21102
|
+
realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
|
|
20979
21103
|
this.#removed = realm.removed();
|
|
20980
21104
|
this.#transport = home.transport();
|
|
20981
21105
|
home.start();
|
|
@@ -21076,7 +21200,11 @@ class Daemon {
|
|
|
21076
21200
|
return {
|
|
21077
21201
|
"daemon.status": () => this.status(),
|
|
21078
21202
|
"device.status": () => this.#deviceStatus(),
|
|
21079
|
-
"device.bind": (p) =>
|
|
21203
|
+
"device.bind": async (p) => {
|
|
21204
|
+
if (this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#directory)
|
|
21205
|
+
return this.#replaceWith(p.token, p.label);
|
|
21206
|
+
return this.#homeDirectory("device.bind").bind(this.#jellyHost ? { ...p, label: this.#jellyHost.label, kind: this.#jellyHost.kind } : p);
|
|
21207
|
+
},
|
|
21080
21208
|
"device.list": () => this.#directory?.list() ?? this.#rosterDevices(),
|
|
21081
21209
|
"device.remove": (p) => this.#homeDirectory("device.remove").remove(p),
|
|
21082
21210
|
"device.link": (p) => this.#homeDirectory("device.link").link(p),
|
|
@@ -21089,6 +21217,10 @@ class Daemon {
|
|
|
21089
21217
|
"device.login.cancel": () => this.#homeDirectory("device.login.cancel").loginCancel(),
|
|
21090
21218
|
"device.approve.preview": (p) => this.#homeDirectory("device.approve.preview").approvePreview(p),
|
|
21091
21219
|
"device.approve": (p) => this.#homeDirectory("device.approve").approve(p),
|
|
21220
|
+
"identity.replace": (p) => this.#replace(p),
|
|
21221
|
+
"identity.replace.preview": (p) => this.#replacePreview(p),
|
|
21222
|
+
"directory.erase": (p) => this.#erase(p),
|
|
21223
|
+
"directory.erase.preview": (p) => this.#erasePreview(p),
|
|
21092
21224
|
"daemon.stop": () => this.#stop(),
|
|
21093
21225
|
"session.register": (p, ctx) => this.#register(p, ctx),
|
|
21094
21226
|
"session.update": (p, ctx) => this.#updateSession(this.#bound(p, ctx)),
|
|
@@ -21189,10 +21321,30 @@ class Daemon {
|
|
|
21189
21321
|
const out = [];
|
|
21190
21322
|
for (const person of all) {
|
|
21191
21323
|
const org = realm.kind === "roster" ? "same" : await realm.profile(person).then((profile) => profile.org, () => "none");
|
|
21192
|
-
|
|
21324
|
+
const reach = this.#cards.reach(person, org === "same");
|
|
21325
|
+
const changedAt = this.#recentDeviceChanges(realm, person).at(0)?.at;
|
|
21326
|
+
out.push(changedAt === undefined ? reach : { ...reach, devicesChangedAt: changedAt });
|
|
21193
21327
|
}
|
|
21194
21328
|
return out;
|
|
21195
21329
|
}
|
|
21330
|
+
#recentDeviceChanges(realm, person) {
|
|
21331
|
+
const since = this.#now() - DEVICES_CHANGED_MS;
|
|
21332
|
+
const changes = realm.deviceChanges(person);
|
|
21333
|
+
const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
|
|
21334
|
+
return changes.filter((change) => change.change !== "vouched" && change.at > since).reverse().slice(0, 10).map((change) => {
|
|
21335
|
+
const approver = change.approver !== undefined ? labelOf(change.approver) : undefined;
|
|
21336
|
+
return {
|
|
21337
|
+
change: change.change,
|
|
21338
|
+
device: change.device,
|
|
21339
|
+
label: MachineLabel.safeParse(change.label).success ? change.label : "device",
|
|
21340
|
+
...change.kind !== undefined ? { kind: change.kind } : {},
|
|
21341
|
+
vouch: change.vouch === "service" ? "device" : change.vouch,
|
|
21342
|
+
...change.via !== undefined ? { via: change.via } : {},
|
|
21343
|
+
...approver !== undefined && MachineLabel.safeParse(approver).success ? { approver } : {},
|
|
21344
|
+
at: change.at
|
|
21345
|
+
};
|
|
21346
|
+
});
|
|
21347
|
+
}
|
|
21196
21348
|
async#grantSubject(p) {
|
|
21197
21349
|
const { realm } = this.#joined();
|
|
21198
21350
|
if ("person" in p.subject)
|
|
@@ -21293,6 +21445,7 @@ class Daemon {
|
|
|
21293
21445
|
now: this.#now
|
|
21294
21446
|
});
|
|
21295
21447
|
realm.on("devices-changed", () => this.#realmChanged());
|
|
21448
|
+
realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
|
|
21296
21449
|
return realm;
|
|
21297
21450
|
}
|
|
21298
21451
|
#admin() {
|
|
@@ -21491,7 +21644,7 @@ class Daemon {
|
|
|
21491
21644
|
type: "presence",
|
|
21492
21645
|
from: { person: me.person, device: identity.machine },
|
|
21493
21646
|
to: { team: true },
|
|
21494
|
-
payload: presencePayload(final ? this.#store.sessions().map((s) => ({ ...s, unregisteredAt: s.unregisteredAt ?? this.#now() })) : this.#store.sessions(), me.label, this.#store.next("presence"),
|
|
21647
|
+
payload: presencePayload(final ? this.#store.sessions().map((s) => ({ ...s, unregisteredAt: s.unregisteredAt ?? this.#now() })) : this.#store.sessions(), me.label, this.#store.next("presence"), {
|
|
21495
21648
|
wake: (s) => this.#wakes(s),
|
|
21496
21649
|
share: new Set(this.#currentConfig().presence?.share ?? []),
|
|
21497
21650
|
away: this.#away()
|
|
@@ -21641,7 +21794,6 @@ class Daemon {
|
|
|
21641
21794
|
kind: p.kind,
|
|
21642
21795
|
message,
|
|
21643
21796
|
wake: p.wake ?? true,
|
|
21644
|
-
...p.external !== undefined ? { external: p.external } : {},
|
|
21645
21797
|
...chain !== undefined ? { delegation: chain.grants } : {}
|
|
21646
21798
|
};
|
|
21647
21799
|
const replyTo = p.replyTo ?? follow?.request;
|
|
@@ -21717,7 +21869,7 @@ class Daemon {
|
|
|
21717
21869
|
}
|
|
21718
21870
|
async#sendLine(p) {
|
|
21719
21871
|
const session = this.#sessionOf(p.key);
|
|
21720
|
-
const extra = ["contextId", "taskId", "messageId", "metadata", "extensions", "referenceTaskIds", "
|
|
21872
|
+
const extra = ["contextId", "taskId", "messageId", "metadata", "extensions", "referenceTaskIds", "onBehalfOf", "wake"].filter((name) => p[name] !== undefined);
|
|
21721
21873
|
if (p.kind !== "message")
|
|
21722
21874
|
throw new MessagingError("invalid_request", "a task goes to an agent, person/agent");
|
|
21723
21875
|
if (extra.length > 0)
|
|
@@ -21790,7 +21942,9 @@ class Daemon {
|
|
|
21790
21942
|
self: device.machine === identity.machine,
|
|
21791
21943
|
...away(device.machine) ? { away: true } : {}
|
|
21792
21944
|
}));
|
|
21793
|
-
|
|
21945
|
+
const own = p.person === this.#ownPerson();
|
|
21946
|
+
const deviceChanges = own ? [] : this.#recentDeviceChanges(realm, p.person);
|
|
21947
|
+
return { person: p.person, ...profile, devices, ...deviceChanges.length > 0 ? { deviceChanges } : {} };
|
|
21794
21948
|
}
|
|
21795
21949
|
#seal(type, toMachine, session, payload, extra = {}) {
|
|
21796
21950
|
const { identity, realm, me } = this.#joined();
|
|
@@ -22306,7 +22460,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22306
22460
|
if (payload.kind === "task") {
|
|
22307
22461
|
const { realm, me } = this.#joined();
|
|
22308
22462
|
try {
|
|
22309
|
-
inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession, task: payload.message.taskId
|
|
22463
|
+
inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession, task: payload.message.taskId });
|
|
22310
22464
|
} catch (error) {
|
|
22311
22465
|
return reject(codeOf(error));
|
|
22312
22466
|
}
|
|
@@ -22336,10 +22490,9 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22336
22490
|
const recipient = this.#store.sessionByName(toSession);
|
|
22337
22491
|
const config = this.#currentConfig();
|
|
22338
22492
|
const code = inboundPolicy({
|
|
22339
|
-
sender: { machine: sender.machine,
|
|
22493
|
+
sender: { machine: sender.machine, session: fromSession, agent: this.#agentOf(sender.machine, fromSession) },
|
|
22340
22494
|
...recipient ? { recipient: { session: recipient.name, agent: recipient.agent } } : {},
|
|
22341
|
-
task: payload.kind === "task"
|
|
22342
|
-
external: payload.external !== undefined
|
|
22495
|
+
task: payload.kind === "task"
|
|
22343
22496
|
}, this.#policy, now);
|
|
22344
22497
|
if (code !== undefined || !recipient)
|
|
22345
22498
|
return reject(code ?? "unknown_session");
|
|
@@ -22357,7 +22510,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22357
22510
|
return reject("invalid_request");
|
|
22358
22511
|
const { realm, me } = this.#joined();
|
|
22359
22512
|
try {
|
|
22360
|
-
chain = inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession: recipient.name, task: taskId
|
|
22513
|
+
chain = inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession: recipient.name, task: taskId });
|
|
22361
22514
|
} catch (error) {
|
|
22362
22515
|
return reject(codeOf(error));
|
|
22363
22516
|
}
|
|
@@ -22374,7 +22527,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22374
22527
|
return reject("too_large");
|
|
22375
22528
|
}
|
|
22376
22529
|
const from = peerRef(sender, fromSession);
|
|
22377
|
-
const message = withProvenance(payload.message, provenanceOf3(env, sender, now
|
|
22530
|
+
const message = withProvenance(payload.message, provenanceOf3(env, sender, now));
|
|
22378
22531
|
const item = {
|
|
22379
22532
|
id: env.id,
|
|
22380
22533
|
key: recipient.key,
|
|
@@ -22439,7 +22592,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22439
22592
|
this.#dropped("not_found", env.id);
|
|
22440
22593
|
return;
|
|
22441
22594
|
}
|
|
22442
|
-
if (
|
|
22595
|
+
if (env.replyTo !== row.request) {
|
|
22443
22596
|
this.#dropped("invalid_request", env.id);
|
|
22444
22597
|
return;
|
|
22445
22598
|
}
|
|
@@ -22746,9 +22899,6 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22746
22899
|
onPeer: (machine, online) => this.#onPeer(machine, online),
|
|
22747
22900
|
onState: (state) => this.#onState(state),
|
|
22748
22901
|
...this.#tuning.reconnect ? { reconnect: this.#tuning.reconnect } : {},
|
|
22749
|
-
...this.#config.transport?.direct ? { direct: this.#config.transport.direct } : {},
|
|
22750
|
-
member: (machine) => this.#removed ? undefined : this.#realm?.device(machine),
|
|
22751
|
-
presence: (machine) => this.#store.directoryRow(machine)?.presence,
|
|
22752
22902
|
log: this.#log
|
|
22753
22903
|
});
|
|
22754
22904
|
this.#transport = transport;
|
|
@@ -22881,7 +23031,6 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22881
23031
|
this.#stopRemoved();
|
|
22882
23032
|
return;
|
|
22883
23033
|
}
|
|
22884
|
-
this.#transport?.rosterChanged?.();
|
|
22885
23034
|
this.#sealedTo.clear();
|
|
22886
23035
|
const gone = hosted ? "unknown_device" : "not_member";
|
|
22887
23036
|
this.#tx(() => {
|
|
@@ -22927,8 +23076,14 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22927
23076
|
await transport?.stop().catch(() => {});
|
|
22928
23077
|
}
|
|
22929
23078
|
#deviceStatus() {
|
|
22930
|
-
if (this.#directory)
|
|
22931
|
-
|
|
23079
|
+
if (this.#directory) {
|
|
23080
|
+
const status = this.#directory.status();
|
|
23081
|
+
return this.#store.getKv(PREDECESSOR_KEY) !== undefined && status.binding !== "removed" ? { ...status, binding: "replace" } : status;
|
|
23082
|
+
}
|
|
23083
|
+
if (this.#config.realm) {
|
|
23084
|
+
const person = this.#store.directoryCache().own().person;
|
|
23085
|
+
return { realm: "directory", binding: "none", ...person ? { person } : {} };
|
|
23086
|
+
}
|
|
22932
23087
|
const identity = this.#identity;
|
|
22933
23088
|
const me = identity ? this.#realm?.device(identity.machine) : undefined;
|
|
22934
23089
|
const binding = this.#removed ? "removed" : me ? "bound" : "none";
|
|
@@ -23065,6 +23220,232 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
23065
23220
|
const { rows, more } = this.#store.conversation(session.key, p.conversation.slice("agent:".length), { before: p.before }, p.limit ?? 30);
|
|
23066
23221
|
return { kind: "entries", conversation: p.conversation, entries: rows.map(convEntry), more };
|
|
23067
23222
|
}
|
|
23223
|
+
#heldRealm(config) {
|
|
23224
|
+
if (this.#directory)
|
|
23225
|
+
return this.#directory.realm;
|
|
23226
|
+
const throwaway = generateIdentity();
|
|
23227
|
+
return new DirectoryIdentity({ config, identity: () => throwaway, link: () => {
|
|
23228
|
+
return;
|
|
23229
|
+
}, cache: memoryDirectoryCache(this.#store.directoryCache()), jelly: this.#jelly ?? jellyFromIssuer(config.issuer), now: this.#now });
|
|
23230
|
+
}
|
|
23231
|
+
#predecessor() {
|
|
23232
|
+
return this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(MACHINE_KEY) ?? this.#identity?.machine;
|
|
23233
|
+
}
|
|
23234
|
+
async#replacePreview(p) {
|
|
23235
|
+
const config = this.#config.realm;
|
|
23236
|
+
if (!config)
|
|
23237
|
+
throw new MessagingError("invalid_state", "identity.replace is for a directory realm; a roster realm replaces a machine with team.remove and a new invite");
|
|
23238
|
+
const person = this.#store.directoryCache().own().person;
|
|
23239
|
+
if (!person)
|
|
23240
|
+
throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
|
|
23241
|
+
const realm = this.#heldRealm(config);
|
|
23242
|
+
const pred = this.#predecessor();
|
|
23243
|
+
const record = realm.ownDevices().find((d) => d.device === pred);
|
|
23244
|
+
if (!record)
|
|
23245
|
+
throw new MessagingError("not_found", "this home's device is not in its account's log");
|
|
23246
|
+
const pending = this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#identity !== undefined && this.#identity.machine !== pred;
|
|
23247
|
+
const bound = pending && realm.binding().state !== "none";
|
|
23248
|
+
const signIns = !pending ? 2 : !bound ? 2 : record.current ? 1 : 0;
|
|
23249
|
+
const alarm = this.#keysMissing ? "keys_missing" : realm.alarm()?.kind === "restored" ? "restored" : null;
|
|
23250
|
+
const label = record.label;
|
|
23251
|
+
const why = alarm === "keys_missing" ? " (its key files are missing)" : alarm === "restored" ? " (it runs from an older copy of its home)" : "";
|
|
23252
|
+
const sentence = `This device gets a new key and replaces '${label}'${why} on your account. Your conversations, grants and history stay here, and tasks continue; ` + `mail still queued at the relay for '${label}' is lost.` + (signIns === 2 ? " Through the browser you approve two sign-ins at Jelly ID: one binds the new key, one removes the old." : signIns === 1 ? ` You approve one sign-in at Jelly ID, to remove '${label}'.` : "");
|
|
23253
|
+
return {
|
|
23254
|
+
predecessor: {
|
|
23255
|
+
device: pred,
|
|
23256
|
+
label,
|
|
23257
|
+
kind: record.kind,
|
|
23258
|
+
vouch: record.vouch,
|
|
23259
|
+
...record.vouch === "jelly" ? { via: record.host !== undefined ? "host" : "browser" } : {},
|
|
23260
|
+
since: record.since,
|
|
23261
|
+
current: record.current
|
|
23262
|
+
},
|
|
23263
|
+
alarm,
|
|
23264
|
+
...pending ? { device: this.#identity.machine } : {},
|
|
23265
|
+
signIns,
|
|
23266
|
+
sentence: sentence.slice(0, 600)
|
|
23267
|
+
};
|
|
23268
|
+
}
|
|
23269
|
+
async#replace(p) {
|
|
23270
|
+
const config = this.#config.realm;
|
|
23271
|
+
if (!config)
|
|
23272
|
+
throw new MessagingError("invalid_state", "identity.replace is for a directory realm; a roster realm replaces a machine with team.remove and a new invite");
|
|
23273
|
+
if (this.#removed || this.#directory?.realm.removed())
|
|
23274
|
+
throw new MessagingError("invalid_state", "this device was removed from its account; it cannot replace anything");
|
|
23275
|
+
const person = this.#store.directoryCache().own().person;
|
|
23276
|
+
if (!person)
|
|
23277
|
+
throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
|
|
23278
|
+
const pred = this.#predecessor();
|
|
23279
|
+
if (!pred)
|
|
23280
|
+
throw new MessagingError("no_identity", "this home names no device to replace");
|
|
23281
|
+
const record = this.#heldRealm(config).ownDevices().find((d) => d.device === pred);
|
|
23282
|
+
if (!record)
|
|
23283
|
+
throw new MessagingError("not_found", "this home's device is not in its account's log");
|
|
23284
|
+
if (this.#store.getKv(PREDECESSOR_KEY) === undefined || this.#identity === undefined || this.#identity.machine === pred) {
|
|
23285
|
+
if (!record.current)
|
|
23286
|
+
throw new MessagingError("invalid_state", "this home's device was removed from its account; it cannot be replaced");
|
|
23287
|
+
await this.#newKeys(pred, config);
|
|
23288
|
+
}
|
|
23289
|
+
return this.#replaceNext(p);
|
|
23290
|
+
}
|
|
23291
|
+
async#replaceNext(p) {
|
|
23292
|
+
const home = this.#directory;
|
|
23293
|
+
const me = this.#identity.machine;
|
|
23294
|
+
if (p.token !== undefined) {
|
|
23295
|
+
const done = await this.#replaceWith(p.token, p.label);
|
|
23296
|
+
if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
|
|
23297
|
+
return { state: "done", person: done.person, device: me, predecessor: done.predecessor, seq: done.seq };
|
|
23298
|
+
} else if (this.#replaceFinished()) {
|
|
23299
|
+
return this.#replaceDone();
|
|
23300
|
+
}
|
|
23301
|
+
if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
|
|
23302
|
+
return this.#replaceDone();
|
|
23303
|
+
if (this.#jellyHost || p.token !== undefined)
|
|
23304
|
+
return { state: "token", device: me };
|
|
23305
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY);
|
|
23306
|
+
const record = home.realm.ownDevices().find((d) => d.device === pred);
|
|
23307
|
+
const label = p.label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device";
|
|
23308
|
+
const kind = record && record.kind !== "jelly-host" ? record.kind : "laptop";
|
|
23309
|
+
const started = await home.signIn("replace", { label, kind });
|
|
23310
|
+
return { state: "sign-in", device: me, ...started };
|
|
23311
|
+
}
|
|
23312
|
+
#replaceFinished() {
|
|
23313
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY);
|
|
23314
|
+
if (pred === undefined)
|
|
23315
|
+
return true;
|
|
23316
|
+
const realm = this.#directory?.realm;
|
|
23317
|
+
if (!realm || realm.binding().state === "none")
|
|
23318
|
+
return false;
|
|
23319
|
+
return realm.ownDevices().find((d) => d.device === pred)?.current !== true;
|
|
23320
|
+
}
|
|
23321
|
+
#replaceDone() {
|
|
23322
|
+
const realm = this.#directory.realm;
|
|
23323
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(REPLACED_KEY);
|
|
23324
|
+
this.#store.deleteKv(PREDECESSOR_KEY);
|
|
23325
|
+
const me = this.#identity.machine;
|
|
23326
|
+
const binding = realm.binding();
|
|
23327
|
+
const record = pred !== undefined ? realm.ownDevices().find((d) => d.device === pred) : undefined;
|
|
23328
|
+
return { state: "done", person: binding.person, device: me, predecessor: pred ?? me, seq: record?.unbound ? realm.deviceChanges(binding.person).find((c) => c.device === pred && c.change === "replaced")?.seq ?? 0 : 0 };
|
|
23329
|
+
}
|
|
23330
|
+
async#replaceWith(token, label) {
|
|
23331
|
+
const home = this.#directory;
|
|
23332
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY);
|
|
23333
|
+
if (!home || pred === undefined)
|
|
23334
|
+
throw new MessagingError("invalid_state", "no replacement is pending: start one with identity.replace");
|
|
23335
|
+
const realm = home.realm;
|
|
23336
|
+
const me = this.#identity.machine;
|
|
23337
|
+
const record = realm.ownDevices().find((d) => d.device === pred);
|
|
23338
|
+
let seq = 0;
|
|
23339
|
+
let spent = false;
|
|
23340
|
+
if (realm.binding().state === "none") {
|
|
23341
|
+
const kind = this.#jellyHost?.kind ?? (record && record.kind !== "jelly-host" ? record.kind : "laptop");
|
|
23342
|
+
const done = await home.bind({ token, label: this.#jellyHost?.label ?? label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device", kind });
|
|
23343
|
+
seq = done.seq;
|
|
23344
|
+
spent = signInToken(token);
|
|
23345
|
+
this.#log("info", "identity.replace.bound", { seq });
|
|
23346
|
+
}
|
|
23347
|
+
await realm.awaitSelf();
|
|
23348
|
+
this.#tx(() => this.#grants.resignFrom(pred));
|
|
23349
|
+
let unbound = false;
|
|
23350
|
+
if (!spent && realm.ownDevices().find((d) => d.device === pred)?.current) {
|
|
23351
|
+
const done = await realm.unbind(pred, { reason: "replaced", token });
|
|
23352
|
+
seq = done.seq;
|
|
23353
|
+
unbound = true;
|
|
23354
|
+
this.#log("info", "identity.replace.done", { seq });
|
|
23355
|
+
}
|
|
23356
|
+
if (unbound || this.#replaceFinished()) {
|
|
23357
|
+
this.#store.setKv(REPLACED_KEY, pred);
|
|
23358
|
+
this.#store.deleteKv(PREDECESSOR_KEY);
|
|
23359
|
+
this.#store.deleteKv(REPLACE_LABEL_KEY);
|
|
23360
|
+
this.#emit({ type: "contacts" });
|
|
23361
|
+
}
|
|
23362
|
+
return { person: realm.binding().person, device: me, predecessor: pred, seq };
|
|
23363
|
+
}
|
|
23364
|
+
async#newKeys(pred, config) {
|
|
23365
|
+
const label = this.#heldRealm(config).ownDevices().find((d) => d.device === pred)?.label;
|
|
23366
|
+
this.#store.setKv(PREDECESSOR_KEY, pred);
|
|
23367
|
+
if (label !== undefined)
|
|
23368
|
+
this.#store.setKv(REPLACE_LABEL_KEY, label);
|
|
23369
|
+
const keys = { identity: generateIdentity(), retained: [] };
|
|
23370
|
+
let identityStore = this.#keyring.store;
|
|
23371
|
+
if (identityStore)
|
|
23372
|
+
identityStore.save(keys);
|
|
23373
|
+
else
|
|
23374
|
+
identityStore = saveNewIdentity(this.paths.identity, keys, identitySettings(this.#config), this.#storeDeps());
|
|
23375
|
+
this.#store.setKv(MACHINE_KEY, keys.identity.machine);
|
|
23376
|
+
this.#log("info", "identity.replace.keys", { store: identityStore.kind });
|
|
23377
|
+
const old = this.#directory;
|
|
23378
|
+
this.#directory = undefined;
|
|
23379
|
+
this.#realm = undefined;
|
|
23380
|
+
this.#transport = undefined;
|
|
23381
|
+
if (old) {
|
|
23382
|
+
await old.stop();
|
|
23383
|
+
this.#onHostedState(false);
|
|
23384
|
+
}
|
|
23385
|
+
const cache = this.#store.directoryCache();
|
|
23386
|
+
if (cache.alarm()?.kind === "restored")
|
|
23387
|
+
cache.setAlarm(undefined);
|
|
23388
|
+
for (const row of this.#store.outbox())
|
|
23389
|
+
if (row.env.from.device === pred)
|
|
23390
|
+
this.#store.deleteOutbox(row.id);
|
|
23391
|
+
this.#keysMissing = false;
|
|
23392
|
+
this.#keyring.adopt(keys, identityStore);
|
|
23393
|
+
this.#openDirectory(config, keys, identityStore);
|
|
23394
|
+
}
|
|
23395
|
+
#erasePreview(_p) {
|
|
23396
|
+
const home = this.#homeDirectory("directory.erase.preview");
|
|
23397
|
+
const binding = home.realm.binding();
|
|
23398
|
+
if (!binding.person || binding.state === "none" || binding.state === "removed")
|
|
23399
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
23400
|
+
const devices = home.realm.ownDevices().filter((d) => d.current);
|
|
23401
|
+
const me = this.#identity.machine;
|
|
23402
|
+
return {
|
|
23403
|
+
person: binding.person,
|
|
23404
|
+
devices: devices.map((d) => ({ device: d.device, label: d.label, kind: d.kind, self: d.device === me })),
|
|
23405
|
+
signIns: this.#jellyHost || binding.record?.host !== undefined ? 0 : 1,
|
|
23406
|
+
perDay: ERASURES_PER_DAY,
|
|
23407
|
+
lost: [
|
|
23408
|
+
`Every device of your account (${devices.length}, this one included) is removed from Messaging, and none can be bound again.`,
|
|
23409
|
+
"Your Jelly account can never be bound to Messaging again: the erasure is for good, and a new sign-in is refused.",
|
|
23410
|
+
"The directory deletes your name, email and handle, and every device record and key statement it held for you.",
|
|
23411
|
+
"Messages still queued at the relay for your devices are deleted.",
|
|
23412
|
+
"Your contacts see a deleted account, and drop the grants and cards they hold for you."
|
|
23413
|
+
],
|
|
23414
|
+
kept: ["Each device keeps its own history (conversations, transcripts, grants) on its disk until you delete its home.", "Your Jelly account itself is not deleted: only its Messaging account is."]
|
|
23415
|
+
};
|
|
23416
|
+
}
|
|
23417
|
+
async#erase(p) {
|
|
23418
|
+
const home = this.#homeDirectory("directory.erase");
|
|
23419
|
+
const binding = home.realm.binding();
|
|
23420
|
+
if (!binding.person || binding.state === "none" || binding.state === "removed")
|
|
23421
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
23422
|
+
if (binding.record?.vouch !== "jelly")
|
|
23423
|
+
throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
|
|
23424
|
+
if (p.token !== undefined) {
|
|
23425
|
+
const done = await this.#eraseWith(p.token);
|
|
23426
|
+
return { state: "done", person: done.person, devices: done.devices };
|
|
23427
|
+
}
|
|
23428
|
+
if (this.#jellyHost)
|
|
23429
|
+
throw new MessagingError("invalid_request", "under a Jelly host, directory.erase needs a fresh token for this device from its host (token)");
|
|
23430
|
+
const record = binding.record;
|
|
23431
|
+
const started = await home.signIn("erase", { label: record.label, kind: record.kind !== "jelly-host" ? record.kind : "laptop" });
|
|
23432
|
+
return { state: "sign-in", ...started };
|
|
23433
|
+
}
|
|
23434
|
+
async#eraseWith(token) {
|
|
23435
|
+
const home = this.#homeDirectory("directory.erase");
|
|
23436
|
+
let done;
|
|
23437
|
+
try {
|
|
23438
|
+
done = await home.realm.erase(token);
|
|
23439
|
+
} catch (error) {
|
|
23440
|
+
if (!(error instanceof MessagingError) || error.code !== "rate_limited")
|
|
23441
|
+
throw error;
|
|
23442
|
+
const wait = error.retryAfterMs;
|
|
23443
|
+
const when = typeof wait === "number" && wait > 0 ? `; try again in about ${Math.max(1, Math.ceil(wait / 60000))} min` : "; try again tomorrow";
|
|
23444
|
+
throw new MessagingError("rate_limited", `the directory allows ${ERASURES_PER_DAY} erasures per person a day${when}`);
|
|
23445
|
+
}
|
|
23446
|
+
this.#log("warn", "directory.erased", { devices: done.devices });
|
|
23447
|
+
return done;
|
|
23448
|
+
}
|
|
23068
23449
|
#removePreview(p) {
|
|
23069
23450
|
const entry = this.#homeDirectory("device.remove.preview").list().devices.find((d) => d.device === p.device);
|
|
23070
23451
|
if (!entry)
|
|
@@ -23078,6 +23459,95 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
23078
23459
|
...entry.since !== undefined ? { since: entry.since } : {}
|
|
23079
23460
|
};
|
|
23080
23461
|
}
|
|
23462
|
+
#deviceLogGrew(persons) {
|
|
23463
|
+
try {
|
|
23464
|
+
this.#deviceNotices();
|
|
23465
|
+
} catch (error) {
|
|
23466
|
+
this.#log("error", "notice.device.failed", { code: codeOf(error) });
|
|
23467
|
+
}
|
|
23468
|
+
if (persons.some((person) => person !== this.#me()))
|
|
23469
|
+
this.#emit({ type: "contacts" });
|
|
23470
|
+
}
|
|
23471
|
+
#ownPerson() {
|
|
23472
|
+
return this.#directory?.person() ?? this.#me();
|
|
23473
|
+
}
|
|
23474
|
+
#deviceNotices(start = false) {
|
|
23475
|
+
const realm = this.#realm ?? this.#directory?.realm;
|
|
23476
|
+
const person = this.#ownPerson();
|
|
23477
|
+
const me = this.#identity?.machine;
|
|
23478
|
+
if (!realm || !person || !me)
|
|
23479
|
+
return;
|
|
23480
|
+
const changes = realm.deviceChanges(person);
|
|
23481
|
+
const head = changes.at(-1)?.seq ?? -1;
|
|
23482
|
+
let last = this.#store.getKv(DEVICE_NOTICES_KEY);
|
|
23483
|
+
if (last === undefined) {
|
|
23484
|
+
const admitted = changes.find((change) => change.device === me && change.change === "added");
|
|
23485
|
+
if (!start && changes.length === 0)
|
|
23486
|
+
return;
|
|
23487
|
+
if (start || admitted === undefined) {
|
|
23488
|
+
this.#store.setKv(DEVICE_NOTICES_KEY, String(head));
|
|
23489
|
+
return;
|
|
23490
|
+
}
|
|
23491
|
+
last = String(admitted.seq);
|
|
23492
|
+
}
|
|
23493
|
+
const fresh = changes.filter((change) => change.seq > Number(last));
|
|
23494
|
+
if (fresh.length === 0)
|
|
23495
|
+
return;
|
|
23496
|
+
const replacers = new Set(fresh.filter((change) => change.change === "replaced").map((change) => change.by));
|
|
23497
|
+
for (const change of fresh) {
|
|
23498
|
+
if (change.device === me || change.by === me)
|
|
23499
|
+
continue;
|
|
23500
|
+
if (change.change === "added" && replacers.has(change.device))
|
|
23501
|
+
continue;
|
|
23502
|
+
this.#notices.device(change.seq, change.at);
|
|
23503
|
+
}
|
|
23504
|
+
this.#store.setKv(DEVICE_NOTICES_KEY, String(Math.max(head, Number(last))));
|
|
23505
|
+
}
|
|
23506
|
+
#noticeDevice(seq) {
|
|
23507
|
+
const realm = this.#realm ?? this.#directory?.realm;
|
|
23508
|
+
const person = this.#ownPerson();
|
|
23509
|
+
if (!realm || !person)
|
|
23510
|
+
return;
|
|
23511
|
+
const changes = realm.deviceChanges(person);
|
|
23512
|
+
const change = changes.find((c) => c.seq === seq);
|
|
23513
|
+
if (!change)
|
|
23514
|
+
return;
|
|
23515
|
+
const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
|
|
23516
|
+
const gone = (device) => changes.some((c) => c.device === device && (c.change === "removed" || c.change === "replaced"));
|
|
23517
|
+
const subject = change.change === "replaced" ? change.by : change.device;
|
|
23518
|
+
const latest = change.change === "replaced" ? changes.filter((c) => c.device === subject && c.change !== "removed" && c.change !== "replaced").at(-1) : change;
|
|
23519
|
+
const label = latest?.label ?? labelOf(subject) ?? "device";
|
|
23520
|
+
const current = !gone(subject);
|
|
23521
|
+
const view = {
|
|
23522
|
+
change: change.change,
|
|
23523
|
+
device: subject,
|
|
23524
|
+
label: MachineLabel.safeParse(label).success ? label : "device",
|
|
23525
|
+
...latest?.kind !== undefined ? { kind: latest.kind } : {},
|
|
23526
|
+
vouch: ((v) => v === "service" ? "device" : v)(latest?.vouch ?? change.vouch),
|
|
23527
|
+
...(latest?.via ?? change.via) !== undefined ? { via: latest?.via ?? change.via } : {},
|
|
23528
|
+
...change.approver !== undefined && labelOf(change.approver) !== undefined ? { approver: labelOf(change.approver) } : {},
|
|
23529
|
+
...change.change === "replaced" ? { replaces: { device: change.device, label: change.label } } : {},
|
|
23530
|
+
current,
|
|
23531
|
+
at: change.at
|
|
23532
|
+
};
|
|
23533
|
+
const remove = current ? this.#removeWith() : undefined;
|
|
23534
|
+
if (remove)
|
|
23535
|
+
view.remove = remove;
|
|
23536
|
+
return view;
|
|
23537
|
+
}
|
|
23538
|
+
#removeWith() {
|
|
23539
|
+
const home = this.#directory;
|
|
23540
|
+
if (!home)
|
|
23541
|
+
return this.#roster?.isAdmin(this.#identity?.machine ?? "") ? "team" : undefined;
|
|
23542
|
+
const self = home.list().devices.find((device) => device.self && device.current);
|
|
23543
|
+
if (!self || self.vouched !== "jelly")
|
|
23544
|
+
return;
|
|
23545
|
+
return self.via === "browser" ? "sign-in" : "token";
|
|
23546
|
+
}
|
|
23547
|
+
#trustNotices() {
|
|
23548
|
+
for (const grant of this.#grants.expiringTrust(TRUST_NOTICE_MS))
|
|
23549
|
+
this.#notices.trust(grant.id, grant.person, grant.until, this.#now());
|
|
23550
|
+
}
|
|
23081
23551
|
#alerts() {
|
|
23082
23552
|
const realmAlarm = this.#directory?.status().alarm;
|
|
23083
23553
|
const roster = this.#keysMissing ? "keys_missing" : this.#keyring.restored() ? "restored" : undefined;
|
|
@@ -23095,6 +23565,16 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
23095
23565
|
}
|
|
23096
23566
|
if (realmAlarm)
|
|
23097
23567
|
this.#notices.alert(realmAlarm.kind, realmAlarm.at);
|
|
23568
|
+
this.#deviceNotices();
|
|
23569
|
+
if (this.#joinedOrUndefined())
|
|
23570
|
+
this.#trustNotices();
|
|
23571
|
+
}
|
|
23572
|
+
#joinedOrUndefined() {
|
|
23573
|
+
try {
|
|
23574
|
+
return this.#joined();
|
|
23575
|
+
} catch {
|
|
23576
|
+
return;
|
|
23577
|
+
}
|
|
23098
23578
|
}
|
|
23099
23579
|
#homeDirectory(method) {
|
|
23100
23580
|
if (this.#directory)
|
|
@@ -23203,7 +23683,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
23203
23683
|
throw new MessagingError("relay_unavailable", "config.json has no relay url");
|
|
23204
23684
|
let result;
|
|
23205
23685
|
await this.#append((roster) => {
|
|
23206
|
-
const made = createInvite(roster, identity, { person: p.person,
|
|
23686
|
+
const made = createInvite(roster, identity, { person: p.person, ttlMs: p.ttlMs ?? DEFAULT_INVITE_TTL_MS, relay, now: this.#now() });
|
|
23207
23687
|
result = { ticket: made.ticket, inviteId: made.entry.body.inviteId, expires: made.entry.body.expires };
|
|
23208
23688
|
return made.entry;
|
|
23209
23689
|
});
|
|
@@ -23223,8 +23703,8 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
23223
23703
|
const invites = roster.entries().flatMap((entry) => {
|
|
23224
23704
|
if (entry.kind !== "invite.create")
|
|
23225
23705
|
return [];
|
|
23226
|
-
const { inviteId, person,
|
|
23227
|
-
return [{ inviteId, person,
|
|
23706
|
+
const { inviteId, person, expires } = entry.body;
|
|
23707
|
+
return [{ inviteId, person, expires, state: roster.invite(inviteId, now)?.state ?? "expired" }];
|
|
23228
23708
|
});
|
|
23229
23709
|
return { team: { id: roster.team, name: roster.name }, members, invites };
|
|
23230
23710
|
}
|
|
@@ -23247,7 +23727,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
23247
23727
|
}
|
|
23248
23728
|
async#teamAdd(p) {
|
|
23249
23729
|
const { identity } = this.#admin();
|
|
23250
|
-
await this.#append((roster) => createEntry(roster, identity, "member.add", { machine: p.machine, enc: p.enc, person: p.person, role:
|
|
23730
|
+
await this.#append((roster) => createEntry(roster, identity, "member.add", { machine: p.machine, enc: p.enc, person: p.person, role: "member", label: p.label }, this.#now()));
|
|
23251
23731
|
this.#log("info", "team.added", { machine: p.machine, person: p.person });
|
|
23252
23732
|
return { ok: true };
|
|
23253
23733
|
}
|
|
@@ -23364,7 +23844,7 @@ function chainPersons(sender, payload) {
|
|
|
23364
23844
|
function peerRef(member, session) {
|
|
23365
23845
|
return { address: formatAddress(member.person, session, member.label), person: member.person, agent: session, device: member.machine, label: member.label };
|
|
23366
23846
|
}
|
|
23367
|
-
function provenanceOf3(env, sender, now
|
|
23847
|
+
function provenanceOf3(env, sender, now) {
|
|
23368
23848
|
const session = env.from.agent;
|
|
23369
23849
|
return {
|
|
23370
23850
|
envelopeId: env.id,
|
|
@@ -23374,8 +23854,7 @@ function provenanceOf3(env, sender, now, external) {
|
|
|
23374
23854
|
device: sender.machine,
|
|
23375
23855
|
label: sender.label,
|
|
23376
23856
|
verified: "signature",
|
|
23377
|
-
receivedAt: now
|
|
23378
|
-
...external !== undefined ? { external: { principal: external.principal, gateway: sender.machine } } : {}
|
|
23857
|
+
receivedAt: now
|
|
23379
23858
|
};
|
|
23380
23859
|
}
|
|
23381
23860
|
function withProvenance(message, provenance) {
|
|
@@ -23391,7 +23870,7 @@ function plaintextBytes(env) {
|
|
|
23391
23870
|
}
|
|
23392
23871
|
function requestOf(row) {
|
|
23393
23872
|
if (row.request === undefined)
|
|
23394
|
-
throw new MessagingError("invalid_state", "this task
|
|
23873
|
+
throw new MessagingError("invalid_state", "this task has no request to answer");
|
|
23395
23874
|
return row.request;
|
|
23396
23875
|
}
|
|
23397
23876
|
function recipientSession(env) {
|
|
@@ -23463,7 +23942,15 @@ function jellyConfig(path, jelly) {
|
|
|
23463
23942
|
function isLineTarget(to) {
|
|
23464
23943
|
return to.startsWith("#") || !to.includes("/");
|
|
23465
23944
|
}
|
|
23466
|
-
|
|
23945
|
+
function signInToken(token) {
|
|
23946
|
+
try {
|
|
23947
|
+
const claims = parseWire(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
23948
|
+
return typeof claims === "object" && claims !== null && claims.src === "device";
|
|
23949
|
+
} catch {
|
|
23950
|
+
return false;
|
|
23951
|
+
}
|
|
23952
|
+
}
|
|
23953
|
+
var DAY3 = 86400000, KEYCHAIN_LEFTOVERS_KEY = "keychain.stale", DEFAULT_TUNING, KEPT_MS, INPUT_TIMEOUT_MS, TASK_BYTES = 1048576, PREDECESSOR_KEY = "identity.predecessor", REPLACE_LABEL_KEY = "identity.replace.label", REPLACED_KEY = "identity.replaced", ERASURES_PER_DAY = 10, DEVICE_NOTICES_KEY = "notice.devices.seq", TRUST_NOTICE_MS, DEVICES_CHANGED_MS, MACHINE_KEY = "identity.machine", DEFAULT_INVITE_TTL_MS, LIST_BYTES = 1572864;
|
|
23467
23954
|
var init_daemon = __esm(() => {
|
|
23468
23955
|
init_dist();
|
|
23469
23956
|
init_dist2();
|
|
@@ -23480,6 +23967,7 @@ var init_daemon = __esm(() => {
|
|
|
23480
23967
|
init_policy();
|
|
23481
23968
|
init_roster_identity();
|
|
23482
23969
|
init_directory_home();
|
|
23970
|
+
init_directory_identity();
|
|
23483
23971
|
init_jelly3();
|
|
23484
23972
|
init_jelly_env();
|
|
23485
23973
|
init_answer_preview();
|
|
@@ -23509,644 +23997,11 @@ var init_daemon = __esm(() => {
|
|
|
23509
23997
|
};
|
|
23510
23998
|
KEPT_MS = DAY3;
|
|
23511
23999
|
INPUT_TIMEOUT_MS = DAY3;
|
|
24000
|
+
TRUST_NOTICE_MS = 7 * DAY3;
|
|
24001
|
+
DEVICES_CHANGED_MS = 7 * DAY3;
|
|
23512
24002
|
DEFAULT_INVITE_TTL_MS = DAY3;
|
|
23513
24003
|
});
|
|
23514
24004
|
|
|
23515
|
-
// packages/daemon/dist/direct-link.js
|
|
23516
|
-
import { createServer } from "http";
|
|
23517
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
23518
|
-
function unavailable2(message) {
|
|
23519
|
-
return new MessagingError("relay_unavailable", message);
|
|
23520
|
-
}
|
|
23521
|
-
|
|
23522
|
-
class Bucket {
|
|
23523
|
-
capacity;
|
|
23524
|
-
#tokens;
|
|
23525
|
-
#at = Date.now();
|
|
23526
|
-
constructor(capacity) {
|
|
23527
|
-
this.capacity = capacity;
|
|
23528
|
-
this.#tokens = capacity;
|
|
23529
|
-
}
|
|
23530
|
-
take(amount = 1) {
|
|
23531
|
-
const now = Date.now();
|
|
23532
|
-
this.#tokens = Math.min(this.capacity, this.#tokens + (now - this.#at) * this.capacity / 60000);
|
|
23533
|
-
this.#at = now;
|
|
23534
|
-
if (this.#tokens < amount)
|
|
23535
|
-
return false;
|
|
23536
|
-
this.#tokens -= amount;
|
|
23537
|
-
return true;
|
|
23538
|
-
}
|
|
23539
|
-
}
|
|
23540
|
-
|
|
23541
|
-
class Link {
|
|
23542
|
-
peer;
|
|
23543
|
-
role;
|
|
23544
|
-
#ws;
|
|
23545
|
-
#tx;
|
|
23546
|
-
#rx;
|
|
23547
|
-
#owner;
|
|
23548
|
-
#pending = new Map;
|
|
23549
|
-
#deliveries = Promise.resolve();
|
|
23550
|
-
#closed = false;
|
|
23551
|
-
constructor(peer, role, ws, keys, owner) {
|
|
23552
|
-
this.peer = peer;
|
|
23553
|
-
this.role = role;
|
|
23554
|
-
this.#ws = ws;
|
|
23555
|
-
this.#owner = owner;
|
|
23556
|
-
const other = role === "initiator" ? "responder" : "initiator";
|
|
23557
|
-
this.#tx = new DirectCipher(keys.record[role]);
|
|
23558
|
-
this.#rx = new DirectCipher(keys.record[other]);
|
|
23559
|
-
ws.removeAllListeners("message");
|
|
23560
|
-
ws.on("message", (data, isBinary) => {
|
|
23561
|
-
try {
|
|
23562
|
-
this.#onMessage(data, isBinary);
|
|
23563
|
-
} catch {
|
|
23564
|
-
this.close(DIRECT_CLOSE.protocolError);
|
|
23565
|
-
}
|
|
23566
|
-
});
|
|
23567
|
-
ws.on("close", () => this.#onClose());
|
|
23568
|
-
}
|
|
23569
|
-
get open() {
|
|
23570
|
-
return !this.#closed && this.#ws.readyState === WebSocket.OPEN;
|
|
23571
|
-
}
|
|
23572
|
-
send(env, ackMs) {
|
|
23573
|
-
const existing = this.#pending.get(env.id);
|
|
23574
|
-
if (existing)
|
|
23575
|
-
return existing.promise;
|
|
23576
|
-
if (!this.open)
|
|
23577
|
-
return Promise.reject(unavailable2("direct link closed"));
|
|
23578
|
-
const text = JSON.stringify({ t: "send", env });
|
|
23579
|
-
if (Buffer.byteLength(text) > LIMITS.frameBytes)
|
|
23580
|
-
return Promise.reject(new MessagingError("too_large", "envelope exceeds the frame limit"));
|
|
23581
|
-
let entry;
|
|
23582
|
-
const promise = new Promise((resolve, reject) => {
|
|
23583
|
-
const timer = setTimeout(() => this.#settle(env.id, unavailable2("direct peer did not answer in time")), ackMs);
|
|
23584
|
-
timer.unref();
|
|
23585
|
-
entry = { resolve, reject, timer, promise: undefined };
|
|
23586
|
-
});
|
|
23587
|
-
entry.promise = promise;
|
|
23588
|
-
this.#pending.set(env.id, entry);
|
|
23589
|
-
this.#write({ t: "send", env }, text);
|
|
23590
|
-
return promise;
|
|
23591
|
-
}
|
|
23592
|
-
close(code) {
|
|
23593
|
-
if (this.#closed)
|
|
23594
|
-
return;
|
|
23595
|
-
this.#closed = true;
|
|
23596
|
-
this.#failAll();
|
|
23597
|
-
this.#ws.close(code);
|
|
23598
|
-
setTimeout(() => this.#ws.terminate(), 2000).unref();
|
|
23599
|
-
}
|
|
23600
|
-
#onClose() {
|
|
23601
|
-
this.#closed = true;
|
|
23602
|
-
this.#failAll();
|
|
23603
|
-
this.#owner.forget(this);
|
|
23604
|
-
}
|
|
23605
|
-
#failAll() {
|
|
23606
|
-
for (const id of [...this.#pending.keys()])
|
|
23607
|
-
this.#settle(id, unavailable2("direct link closed"));
|
|
23608
|
-
}
|
|
23609
|
-
#settle(id, error) {
|
|
23610
|
-
const entry = this.#pending.get(id);
|
|
23611
|
-
if (!entry)
|
|
23612
|
-
return;
|
|
23613
|
-
this.#pending.delete(id);
|
|
23614
|
-
clearTimeout(entry.timer);
|
|
23615
|
-
if (error)
|
|
23616
|
-
entry.reject(error);
|
|
23617
|
-
else
|
|
23618
|
-
entry.resolve();
|
|
23619
|
-
}
|
|
23620
|
-
#write(record, text = JSON.stringify(record)) {
|
|
23621
|
-
if (this.open)
|
|
23622
|
-
this.#ws.send(this.#tx.seal(Buffer.from(text, "utf8")));
|
|
23623
|
-
}
|
|
23624
|
-
#onMessage(data, isBinary) {
|
|
23625
|
-
if (this.#closed)
|
|
23626
|
-
return;
|
|
23627
|
-
if (!isBinary)
|
|
23628
|
-
return this.close(DIRECT_CLOSE.protocolError);
|
|
23629
|
-
if (!this.#owner.isMember(this.peer))
|
|
23630
|
-
return this.close(DIRECT_CLOSE.notMember);
|
|
23631
|
-
const bytes = bufferOf(data);
|
|
23632
|
-
let raw;
|
|
23633
|
-
try {
|
|
23634
|
-
raw = parseWire(this.#rx.open(bytes).toString("utf8"), { maxBytes: LIMITS.frameBytes });
|
|
23635
|
-
} catch {
|
|
23636
|
-
return this.close(DIRECT_CLOSE.protocolError);
|
|
23637
|
-
}
|
|
23638
|
-
const within = this.#owner.withinRate(this.peer, bytes.length);
|
|
23639
|
-
const parsed = DirectRecord.safeParse(raw);
|
|
23640
|
-
if (!parsed.success) {
|
|
23641
|
-
const id = raw?.env?.id;
|
|
23642
|
-
if (raw?.t === "send" && Ulid.safeParse(id).success)
|
|
23643
|
-
this.#write({ t: "nack", id, code: within ? "invalid_request" : "rate_limited" });
|
|
23644
|
-
return;
|
|
23645
|
-
}
|
|
23646
|
-
const record = parsed.data;
|
|
23647
|
-
if (record.t === "send")
|
|
23648
|
-
return this.#onSend(raw.env, record.env, within);
|
|
23649
|
-
if (!within)
|
|
23650
|
-
return;
|
|
23651
|
-
if (record.t === "ack")
|
|
23652
|
-
this.#settle(record.id);
|
|
23653
|
-
else
|
|
23654
|
-
this.#settle(record.id, new MessagingError(record.code, "refused by the direct peer"));
|
|
23655
|
-
}
|
|
23656
|
-
#onSend(raw, env, within) {
|
|
23657
|
-
const nack = (code) => this.#write({ t: "nack", id: env.id, code });
|
|
23658
|
-
if (!within)
|
|
23659
|
-
return nack("rate_limited");
|
|
23660
|
-
if (env.from.device !== this.peer || env.to.device !== this.#owner.self || env.type === "presence")
|
|
23661
|
-
return nack("invalid_request");
|
|
23662
|
-
this.#deliveries = this.#deliveries.then(async () => {
|
|
23663
|
-
try {
|
|
23664
|
-
await this.#owner.deliver(raw);
|
|
23665
|
-
} catch (error) {
|
|
23666
|
-
return nack(error instanceof MessagingError ? error.code : "internal");
|
|
23667
|
-
}
|
|
23668
|
-
this.#write({ t: "ack", id: env.id });
|
|
23669
|
-
});
|
|
23670
|
-
}
|
|
23671
|
-
}
|
|
23672
|
-
|
|
23673
|
-
class DirectLinks {
|
|
23674
|
-
#options;
|
|
23675
|
-
#limits;
|
|
23676
|
-
#in = new Map;
|
|
23677
|
-
#out = new Map;
|
|
23678
|
-
#dials = new Map;
|
|
23679
|
-
#rates = new Map;
|
|
23680
|
-
#pendingByAddress = new Map;
|
|
23681
|
-
#handshakes;
|
|
23682
|
-
#sockets = new Set;
|
|
23683
|
-
#dialing = new Set;
|
|
23684
|
-
#pending = 0;
|
|
23685
|
-
#server;
|
|
23686
|
-
#wss;
|
|
23687
|
-
#closed = false;
|
|
23688
|
-
stats = { inbound: 0, outbound: 0, refused: 0, dialFailed: 0, delivered: 0 };
|
|
23689
|
-
constructor(options) {
|
|
23690
|
-
this.#options = options;
|
|
23691
|
-
this.#limits = { ...DEFAULT_DIRECT_LIMITS, ...options.limits };
|
|
23692
|
-
this.#handshakes = new Bucket(this.#limits.handshakesPerMinute);
|
|
23693
|
-
}
|
|
23694
|
-
get self() {
|
|
23695
|
-
return this.#options.identity.machine;
|
|
23696
|
-
}
|
|
23697
|
-
isMember(machine) {
|
|
23698
|
-
return machine !== this.self && this.#options.member(machine) !== undefined;
|
|
23699
|
-
}
|
|
23700
|
-
listen(host, port) {
|
|
23701
|
-
if (this.#server)
|
|
23702
|
-
return Promise.reject(new MessagingError("invalid_state", "already listening"));
|
|
23703
|
-
const server = createServer((_req, res) => {
|
|
23704
|
-
res.writeHead(426, { connection: "close" });
|
|
23705
|
-
res.end();
|
|
23706
|
-
});
|
|
23707
|
-
server.headersTimeout = this.#limits.handshakeMs;
|
|
23708
|
-
server.requestTimeout = this.#limits.handshakeMs;
|
|
23709
|
-
server.maxConnections = this.#limits.pending + 256;
|
|
23710
|
-
const wss = new WebSocketServer({ noServer: true, clientTracking: false, ...SOCKET_OPTIONS });
|
|
23711
|
-
server.on("connection", (socket) => {
|
|
23712
|
-
this.#sockets.add(socket);
|
|
23713
|
-
socket.once("close", () => this.#sockets.delete(socket));
|
|
23714
|
-
});
|
|
23715
|
-
server.on("upgrade", (req, socket, head) => this.#upgrade(wss, req, socket, head));
|
|
23716
|
-
this.#server = server;
|
|
23717
|
-
this.#wss = wss;
|
|
23718
|
-
return new Promise((resolve, reject) => {
|
|
23719
|
-
server.once("error", reject);
|
|
23720
|
-
server.listen({ host, port, exclusive: true }, () => {
|
|
23721
|
-
server.off("error", reject);
|
|
23722
|
-
server.on("error", () => {});
|
|
23723
|
-
const bound = server.address();
|
|
23724
|
-
if (bound === null || typeof bound === "string")
|
|
23725
|
-
return reject(new MessagingError("internal", "listener has no address"));
|
|
23726
|
-
resolve({ host: bound.address, port: bound.port });
|
|
23727
|
-
});
|
|
23728
|
-
});
|
|
23729
|
-
}
|
|
23730
|
-
async close() {
|
|
23731
|
-
this.#closed = true;
|
|
23732
|
-
for (const link of [...this.#in.values(), ...this.#out.values()])
|
|
23733
|
-
link.close(1001);
|
|
23734
|
-
for (const ws of this.#dialing)
|
|
23735
|
-
ws.terminate();
|
|
23736
|
-
this.#in.clear();
|
|
23737
|
-
this.#out.clear();
|
|
23738
|
-
const server = this.#server;
|
|
23739
|
-
this.#server = undefined;
|
|
23740
|
-
this.#wss?.close();
|
|
23741
|
-
if (!server)
|
|
23742
|
-
return;
|
|
23743
|
-
await new Promise((resolve) => {
|
|
23744
|
-
server.close(() => resolve());
|
|
23745
|
-
for (const socket of this.#sockets)
|
|
23746
|
-
socket.destroy();
|
|
23747
|
-
});
|
|
23748
|
-
}
|
|
23749
|
-
hasLink(peer) {
|
|
23750
|
-
return this.#linkTo(peer) !== undefined;
|
|
23751
|
-
}
|
|
23752
|
-
async send(peer, endpoints, env) {
|
|
23753
|
-
if (this.#closed)
|
|
23754
|
-
throw unavailable2("direct links are closed");
|
|
23755
|
-
if (!this.isMember(peer))
|
|
23756
|
-
throw new MessagingError("not_member", "not a current member");
|
|
23757
|
-
let link = this.#linkTo(peer);
|
|
23758
|
-
if (!link) {
|
|
23759
|
-
if (endpoints.length === 0)
|
|
23760
|
-
throw unavailable2("no direct link to this machine");
|
|
23761
|
-
link = await this.#dialOnce(peer, endpoints);
|
|
23762
|
-
}
|
|
23763
|
-
await link.send(env, this.#limits.ackMs);
|
|
23764
|
-
}
|
|
23765
|
-
prune() {
|
|
23766
|
-
for (const link of [...this.#in.values(), ...this.#out.values()])
|
|
23767
|
-
if (!this.isMember(link.peer))
|
|
23768
|
-
link.close(DIRECT_CLOSE.notMember);
|
|
23769
|
-
}
|
|
23770
|
-
forget(link) {
|
|
23771
|
-
if (this.#in.get(link.peer) === link)
|
|
23772
|
-
this.#in.delete(link.peer);
|
|
23773
|
-
if (this.#out.get(link.peer) === link)
|
|
23774
|
-
this.#out.delete(link.peer);
|
|
23775
|
-
}
|
|
23776
|
-
withinRate(peer, bytes) {
|
|
23777
|
-
let rate = this.#rates.get(peer);
|
|
23778
|
-
if (!rate) {
|
|
23779
|
-
rate = { frames: new Bucket(this.#limits.framesPerMinute), bytes: new Bucket(this.#limits.bytesPerMinute) };
|
|
23780
|
-
this.#rates.set(peer, rate);
|
|
23781
|
-
}
|
|
23782
|
-
return rate.frames.take() && rate.bytes.take(bytes);
|
|
23783
|
-
}
|
|
23784
|
-
async deliver(raw) {
|
|
23785
|
-
await this.#options.onDeliver(raw);
|
|
23786
|
-
this.stats.delivered++;
|
|
23787
|
-
}
|
|
23788
|
-
#linkTo(peer) {
|
|
23789
|
-
for (const link of [this.#out.get(peer), this.#in.get(peer)])
|
|
23790
|
-
if (link?.open)
|
|
23791
|
-
return link;
|
|
23792
|
-
return;
|
|
23793
|
-
}
|
|
23794
|
-
#upgrade(wss, req, socket, head) {
|
|
23795
|
-
const address = req.socket.remoteAddress ?? "?";
|
|
23796
|
-
const fromAddress = this.#pendingByAddress.get(address) ?? 0;
|
|
23797
|
-
const path = new URL(req.url ?? "/", "http://direct").pathname;
|
|
23798
|
-
if (this.#closed || path !== DIRECT_PATH || this.#pending >= this.#limits.pending || fromAddress >= this.#limits.pendingPerAddress || !this.#handshakes.take()) {
|
|
23799
|
-
this.stats.refused++;
|
|
23800
|
-
socket.destroy();
|
|
23801
|
-
return;
|
|
23802
|
-
}
|
|
23803
|
-
this.#pending++;
|
|
23804
|
-
this.#pendingByAddress.set(address, fromAddress + 1);
|
|
23805
|
-
let released = false;
|
|
23806
|
-
const release = () => {
|
|
23807
|
-
if (released)
|
|
23808
|
-
return;
|
|
23809
|
-
released = true;
|
|
23810
|
-
this.#pending--;
|
|
23811
|
-
const left = (this.#pendingByAddress.get(address) ?? 1) - 1;
|
|
23812
|
-
if (left > 0)
|
|
23813
|
-
this.#pendingByAddress.set(address, left);
|
|
23814
|
-
else
|
|
23815
|
-
this.#pendingByAddress.delete(address);
|
|
23816
|
-
};
|
|
23817
|
-
const timer = setTimeout(() => {
|
|
23818
|
-
release();
|
|
23819
|
-
socket.destroy();
|
|
23820
|
-
}, this.#limits.handshakeMs);
|
|
23821
|
-
timer.unref();
|
|
23822
|
-
socket.once("close", () => {
|
|
23823
|
-
clearTimeout(timer);
|
|
23824
|
-
release();
|
|
23825
|
-
});
|
|
23826
|
-
wss.handleUpgrade(req, socket, head, (ws) => this.#respond(ws, () => {
|
|
23827
|
-
clearTimeout(timer);
|
|
23828
|
-
release();
|
|
23829
|
-
}));
|
|
23830
|
-
}
|
|
23831
|
-
#respond(ws, done) {
|
|
23832
|
-
const { identity, team } = this.#options;
|
|
23833
|
-
let state = { step: "hello" };
|
|
23834
|
-
const refuse = () => {
|
|
23835
|
-
state = { step: "done" };
|
|
23836
|
-
this.stats.refused++;
|
|
23837
|
-
done();
|
|
23838
|
-
ws.close(DIRECT_CLOSE.authFailed);
|
|
23839
|
-
setTimeout(() => ws.terminate(), 2000).unref();
|
|
23840
|
-
};
|
|
23841
|
-
ws.on("error", () => {});
|
|
23842
|
-
ws.on("message", (data, isBinary) => {
|
|
23843
|
-
try {
|
|
23844
|
-
step(data, isBinary);
|
|
23845
|
-
} catch {
|
|
23846
|
-
if (state.step !== "done")
|
|
23847
|
-
refuse();
|
|
23848
|
-
}
|
|
23849
|
-
});
|
|
23850
|
-
ws.on("close", () => done());
|
|
23851
|
-
const step = (data, isBinary) => {
|
|
23852
|
-
if (state.step === "done")
|
|
23853
|
-
return;
|
|
23854
|
-
if (state.step === "hello") {
|
|
23855
|
-
const hello = handshakeFrame(DirectHelloFrame, data, isBinary);
|
|
23856
|
-
if (!hello)
|
|
23857
|
-
return refuse();
|
|
23858
|
-
const own = directEphemeral();
|
|
23859
|
-
const transcript = directTranscript(hello, own.half);
|
|
23860
|
-
let keys;
|
|
23861
|
-
try {
|
|
23862
|
-
keys = directKeys(own.key, hello.epk, transcript);
|
|
23863
|
-
} catch {
|
|
23864
|
-
return refuse();
|
|
23865
|
-
}
|
|
23866
|
-
const sig = sign(identity, DOMAIN.directAuth, directAuthPayload({ transcript, team, role: "responder", machine: identity.machine }));
|
|
23867
|
-
state = { step: "auth", transcript, keys };
|
|
23868
|
-
ws.send(JSON.stringify({ t: "accept", ...own.half, box: sealDirectBox(keys.box.responder, transcript, { team, machine: identity.machine, sig }) }));
|
|
23869
|
-
return;
|
|
23870
|
-
}
|
|
23871
|
-
const { transcript, keys } = state;
|
|
23872
|
-
const auth = handshakeFrame(DirectAuthFrame, data, isBinary);
|
|
23873
|
-
const peer = auth && openIdentity(keys.box.initiator, transcript, auth.box);
|
|
23874
|
-
if (!peer || peer.team !== team || !this.isMember(peer.machine))
|
|
23875
|
-
return refuse();
|
|
23876
|
-
const signed = directAuthPayload({ transcript, team, role: "initiator", machine: peer.machine, peer: identity.machine });
|
|
23877
|
-
if (!verify(peer.machine, DOMAIN.directAuth, signed, peer.sig))
|
|
23878
|
-
return refuse();
|
|
23879
|
-
state = { step: "done" };
|
|
23880
|
-
done();
|
|
23881
|
-
if (this.#closed)
|
|
23882
|
-
return void ws.close(1001);
|
|
23883
|
-
const link = new Link(peer.machine, "responder", ws, keys, this);
|
|
23884
|
-
this.#in.get(peer.machine)?.close(DIRECT_CLOSE.replaced);
|
|
23885
|
-
this.#in.set(peer.machine, link);
|
|
23886
|
-
this.stats.inbound++;
|
|
23887
|
-
};
|
|
23888
|
-
}
|
|
23889
|
-
#dialOnce(peer, endpoints) {
|
|
23890
|
-
let dial = this.#dials.get(peer);
|
|
23891
|
-
if (!dial) {
|
|
23892
|
-
dial = this.#dialAny(peer, endpoints).finally(() => this.#dials.delete(peer));
|
|
23893
|
-
this.#dials.set(peer, dial);
|
|
23894
|
-
}
|
|
23895
|
-
return dial;
|
|
23896
|
-
}
|
|
23897
|
-
async#dialAny(peer, endpoints) {
|
|
23898
|
-
let last;
|
|
23899
|
-
for (const endpoint of endpoints) {
|
|
23900
|
-
try {
|
|
23901
|
-
const link = await this.#dial(peer, endpoint);
|
|
23902
|
-
if (this.#closed) {
|
|
23903
|
-
link.close(1001);
|
|
23904
|
-
break;
|
|
23905
|
-
}
|
|
23906
|
-
this.#out.get(peer)?.close(1000);
|
|
23907
|
-
this.#out.set(peer, link);
|
|
23908
|
-
this.stats.outbound++;
|
|
23909
|
-
return link;
|
|
23910
|
-
} catch (error) {
|
|
23911
|
-
last = error;
|
|
23912
|
-
}
|
|
23913
|
-
}
|
|
23914
|
-
this.stats.dialFailed++;
|
|
23915
|
-
throw last instanceof MessagingError ? last : unavailable2("could not reach the direct peer");
|
|
23916
|
-
}
|
|
23917
|
-
#dial(peer, endpoint) {
|
|
23918
|
-
const { identity, team } = this.#options;
|
|
23919
|
-
const host = endpoint.host.includes(":") ? `[${endpoint.host}]` : endpoint.host;
|
|
23920
|
-
return new Promise((resolve, reject) => {
|
|
23921
|
-
const ws = new WebSocket(`ws://${host}:${endpoint.port}${DIRECT_PATH}`, { ...SOCKET_OPTIONS, handshakeTimeout: this.#limits.dialMs, followRedirects: false });
|
|
23922
|
-
this.#dialing.add(ws);
|
|
23923
|
-
let settled = false;
|
|
23924
|
-
const fail = (message) => {
|
|
23925
|
-
if (settled)
|
|
23926
|
-
return;
|
|
23927
|
-
settled = true;
|
|
23928
|
-
clearTimeout(timer);
|
|
23929
|
-
this.#dialing.delete(ws);
|
|
23930
|
-
ws.removeAllListeners("message");
|
|
23931
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
23932
|
-
ws.close(DIRECT_CLOSE.authFailed);
|
|
23933
|
-
else
|
|
23934
|
-
ws.terminate();
|
|
23935
|
-
reject(unavailable2(message));
|
|
23936
|
-
};
|
|
23937
|
-
const timer = setTimeout(() => fail("direct handshake timed out"), this.#limits.dialMs);
|
|
23938
|
-
timer.unref();
|
|
23939
|
-
const own = directEphemeral();
|
|
23940
|
-
ws.on("error", () => fail("cannot reach the direct peer"));
|
|
23941
|
-
ws.on("close", () => fail("the direct peer closed the link"));
|
|
23942
|
-
ws.on("unexpected-response", () => fail("the direct peer refused the upgrade"));
|
|
23943
|
-
ws.on("open", () => ws.send(JSON.stringify({ t: "hello", proto: DIRECT_PROTOCOL, ...own.half })));
|
|
23944
|
-
ws.on("message", (data, isBinary) => {
|
|
23945
|
-
try {
|
|
23946
|
-
onAccept(data, isBinary);
|
|
23947
|
-
} catch {
|
|
23948
|
-
fail("bad handshake");
|
|
23949
|
-
}
|
|
23950
|
-
});
|
|
23951
|
-
const onAccept = (data, isBinary) => {
|
|
23952
|
-
if (settled)
|
|
23953
|
-
return;
|
|
23954
|
-
const accept = handshakeFrame(DirectAcceptFrame, data, isBinary);
|
|
23955
|
-
if (!accept)
|
|
23956
|
-
return fail("bad handshake frame");
|
|
23957
|
-
const transcript = directTranscript(own.half, accept);
|
|
23958
|
-
let keys;
|
|
23959
|
-
try {
|
|
23960
|
-
keys = directKeys(own.key, accept.epk, transcript);
|
|
23961
|
-
} catch {
|
|
23962
|
-
return fail("unusable handshake key");
|
|
23963
|
-
}
|
|
23964
|
-
const them = openIdentity(keys.box.responder, transcript, accept.box);
|
|
23965
|
-
if (!them || them.team !== team || them.machine !== peer || !this.isMember(peer))
|
|
23966
|
-
return fail("the direct peer is not the expected member");
|
|
23967
|
-
if (!verify(peer, DOMAIN.directAuth, directAuthPayload({ transcript, team, role: "responder", machine: peer }), them.sig))
|
|
23968
|
-
return fail("bad handshake signature");
|
|
23969
|
-
const sig = sign(identity, DOMAIN.directAuth, directAuthPayload({ transcript, team, role: "initiator", machine: identity.machine, peer }));
|
|
23970
|
-
ws.send(JSON.stringify({ t: "auth", box: sealDirectBox(keys.box.initiator, transcript, { team, machine: identity.machine, sig }) }));
|
|
23971
|
-
settled = true;
|
|
23972
|
-
clearTimeout(timer);
|
|
23973
|
-
this.#dialing.delete(ws);
|
|
23974
|
-
ws.removeAllListeners("close");
|
|
23975
|
-
resolve(new Link(peer, "initiator", ws, keys, this));
|
|
23976
|
-
};
|
|
23977
|
-
});
|
|
23978
|
-
}
|
|
23979
|
-
}
|
|
23980
|
-
function handshakeFrame(schema, data, isBinary) {
|
|
23981
|
-
if (isBinary)
|
|
23982
|
-
return;
|
|
23983
|
-
const bytes = bufferOf(data);
|
|
23984
|
-
if (bytes.length > DIRECT_HANDSHAKE_BYTES)
|
|
23985
|
-
return;
|
|
23986
|
-
try {
|
|
23987
|
-
const parsed = schema.safeParse(parseWire(bytes.toString("utf8"), { maxBytes: DIRECT_HANDSHAKE_BYTES }));
|
|
23988
|
-
return parsed.success ? parsed.data : undefined;
|
|
23989
|
-
} catch {
|
|
23990
|
-
return;
|
|
23991
|
-
}
|
|
23992
|
-
}
|
|
23993
|
-
function openIdentity(key, transcript, box) {
|
|
23994
|
-
try {
|
|
23995
|
-
const parsed = DirectIdentity.safeParse(openDirectBox(key, transcript, box));
|
|
23996
|
-
return parsed.success ? parsed.data : undefined;
|
|
23997
|
-
} catch {
|
|
23998
|
-
return;
|
|
23999
|
-
}
|
|
24000
|
-
}
|
|
24001
|
-
function bufferOf(data) {
|
|
24002
|
-
if (Buffer.isBuffer(data))
|
|
24003
|
-
return data;
|
|
24004
|
-
if (Array.isArray(data))
|
|
24005
|
-
return Buffer.concat(data);
|
|
24006
|
-
return Buffer.from(data);
|
|
24007
|
-
}
|
|
24008
|
-
var DIRECT_PATH = "/v1/direct", DIRECT_CLOSE, DEFAULT_DIRECT_LIMITS, RECORD_BYTES, SOCKET_OPTIONS;
|
|
24009
|
-
var init_direct_link = __esm(() => {
|
|
24010
|
-
init_dist();
|
|
24011
|
-
DIRECT_CLOSE = { authFailed: 4001, protocolError: 4002, notMember: 4003, replaced: 4004 };
|
|
24012
|
-
DEFAULT_DIRECT_LIMITS = {
|
|
24013
|
-
pending: 16,
|
|
24014
|
-
pendingPerAddress: 4,
|
|
24015
|
-
handshakeMs: 5000,
|
|
24016
|
-
handshakesPerMinute: 120,
|
|
24017
|
-
dialMs: 2000,
|
|
24018
|
-
ackMs: 5000,
|
|
24019
|
-
framesPerMinute: LIMITS.relayFramesPerMinute,
|
|
24020
|
-
bytesPerMinute: LIMITS.relayBytesPerMinute
|
|
24021
|
-
};
|
|
24022
|
-
RECORD_BYTES = LIMITS.frameBytes + 16;
|
|
24023
|
-
SOCKET_OPTIONS = { maxPayload: RECORD_BYTES, perMessageDeflate: false };
|
|
24024
|
-
});
|
|
24025
|
-
|
|
24026
|
-
// packages/daemon/dist/direct-transport.js
|
|
24027
|
-
function withDirect(relay, tuning = {}) {
|
|
24028
|
-
return (options) => options.direct?.enabled === true && options.team !== undefined ? new DirectTransport(relay, options, options.direct, tuning) : relay(options);
|
|
24029
|
-
}
|
|
24030
|
-
|
|
24031
|
-
class DirectTransport {
|
|
24032
|
-
#relay;
|
|
24033
|
-
#links;
|
|
24034
|
-
#options;
|
|
24035
|
-
#config;
|
|
24036
|
-
#listen;
|
|
24037
|
-
#backoffMs;
|
|
24038
|
-
#maxBackoffMs;
|
|
24039
|
-
#backoff = new Map;
|
|
24040
|
-
#advertised;
|
|
24041
|
-
#running = false;
|
|
24042
|
-
#counts = { direct: 0, relay: 0, fallback: 0 };
|
|
24043
|
-
constructor(relay, options, config, tuning = {}) {
|
|
24044
|
-
this.#options = options;
|
|
24045
|
-
this.#config = config;
|
|
24046
|
-
const listen = parseHostPort(config.listen ?? DEFAULT_DIRECT_LISTEN);
|
|
24047
|
-
this.#listen = { host: listen.host, port: listen.port ?? 0 };
|
|
24048
|
-
this.#backoffMs = tuning.backoffMs ?? 30000;
|
|
24049
|
-
this.#maxBackoffMs = tuning.maxBackoffMs ?? 600000;
|
|
24050
|
-
this.#links = new DirectLinks({
|
|
24051
|
-
identity: options.identity,
|
|
24052
|
-
team: options.team,
|
|
24053
|
-
member: (machine) => options.member?.(machine),
|
|
24054
|
-
onDeliver: (raw) => options.onDeliver(raw),
|
|
24055
|
-
...tuning.limits ? { limits: tuning.limits } : {}
|
|
24056
|
-
});
|
|
24057
|
-
this.#relay = relay(options);
|
|
24058
|
-
}
|
|
24059
|
-
start() {
|
|
24060
|
-
if (this.#running)
|
|
24061
|
-
return;
|
|
24062
|
-
this.#running = true;
|
|
24063
|
-
this.#links.listen(this.#listen.host, this.#listen.port).then((bound) => {
|
|
24064
|
-
if (!this.#running)
|
|
24065
|
-
return;
|
|
24066
|
-
this.#advertised = advertisedEndpoints(this.#config, bound);
|
|
24067
|
-
this.#options.log?.("info", "direct.listening", { host: bound.host, port: bound.port, advertised: this.#advertised.length });
|
|
24068
|
-
}).catch((error) => this.#options.log?.("warn", "direct.listen.failed", { code: codeOf(error) })).finally(() => {
|
|
24069
|
-
if (this.#running)
|
|
24070
|
-
this.#relay.start();
|
|
24071
|
-
});
|
|
24072
|
-
}
|
|
24073
|
-
async stop() {
|
|
24074
|
-
this.#running = false;
|
|
24075
|
-
this.#advertised = undefined;
|
|
24076
|
-
await this.#links.close();
|
|
24077
|
-
await this.#relay.stop();
|
|
24078
|
-
}
|
|
24079
|
-
get connected() {
|
|
24080
|
-
return this.#relay.connected;
|
|
24081
|
-
}
|
|
24082
|
-
get stats() {
|
|
24083
|
-
return { ...this.#links.stats, ...this.#counts };
|
|
24084
|
-
}
|
|
24085
|
-
async send(env) {
|
|
24086
|
-
const to = this.#running && env.type !== "presence" && "device" in env.to ? env.to.device : undefined;
|
|
24087
|
-
if (to !== undefined) {
|
|
24088
|
-
const endpoints = this.#dialable(to);
|
|
24089
|
-
if (endpoints.length > 0 || this.#links.hasLink(to)) {
|
|
24090
|
-
try {
|
|
24091
|
-
await this.#links.send(to, endpoints, env);
|
|
24092
|
-
this.#backoff.delete(to);
|
|
24093
|
-
this.#counts.direct++;
|
|
24094
|
-
return;
|
|
24095
|
-
} catch (error) {
|
|
24096
|
-
this.#counts.fallback++;
|
|
24097
|
-
this.#fail(to);
|
|
24098
|
-
this.#options.log?.("info", "direct.fallback", { id: env.id, code: codeOf(error) });
|
|
24099
|
-
}
|
|
24100
|
-
}
|
|
24101
|
-
}
|
|
24102
|
-
await this.#relay.send(env);
|
|
24103
|
-
this.#counts.relay++;
|
|
24104
|
-
}
|
|
24105
|
-
presence(env) {
|
|
24106
|
-
this.#relay.presence(env);
|
|
24107
|
-
}
|
|
24108
|
-
retryNow() {
|
|
24109
|
-
this.#relay.retryNow?.();
|
|
24110
|
-
}
|
|
24111
|
-
appendRoster(entry) {
|
|
24112
|
-
return this.#relay.appendRoster(entry);
|
|
24113
|
-
}
|
|
24114
|
-
syncRoster(after) {
|
|
24115
|
-
return this.#relay.syncRoster(after);
|
|
24116
|
-
}
|
|
24117
|
-
rosterChanged() {
|
|
24118
|
-
this.#links.prune();
|
|
24119
|
-
}
|
|
24120
|
-
hasLink(machine) {
|
|
24121
|
-
return this.#links.hasLink(machine);
|
|
24122
|
-
}
|
|
24123
|
-
advertise() {
|
|
24124
|
-
const direct = this.#advertised;
|
|
24125
|
-
return direct && direct.length > 0 ? { features: ["direct"], direct } : undefined;
|
|
24126
|
-
}
|
|
24127
|
-
#dialable(machine) {
|
|
24128
|
-
const backoff = this.#backoff.get(machine);
|
|
24129
|
-
if (backoff && Date.now() < backoff.until)
|
|
24130
|
-
return [];
|
|
24131
|
-
if (!this.#options.member?.(machine))
|
|
24132
|
-
return [];
|
|
24133
|
-
const presence = this.#options.presence?.(machine);
|
|
24134
|
-
if (!presence?.features?.includes("direct") || !presence.direct)
|
|
24135
|
-
return [];
|
|
24136
|
-
return presence.direct.filter((endpoint) => mayDial(endpoint, isLoopback(this.#listen.host)));
|
|
24137
|
-
}
|
|
24138
|
-
#fail(machine) {
|
|
24139
|
-
const prior = this.#backoff.get(machine);
|
|
24140
|
-
const ms = prior ? Math.min(prior.ms * 2, this.#maxBackoffMs) : this.#backoffMs;
|
|
24141
|
-
this.#backoff.set(machine, { until: Date.now() + ms, ms });
|
|
24142
|
-
}
|
|
24143
|
-
}
|
|
24144
|
-
var init_direct_transport = __esm(() => {
|
|
24145
|
-
init_direct_config();
|
|
24146
|
-
init_direct_link();
|
|
24147
|
-
init_log();
|
|
24148
|
-
});
|
|
24149
|
-
|
|
24150
24005
|
// packages/relay/dist/frames.js
|
|
24151
24006
|
function text3(data) {
|
|
24152
24007
|
if (Buffer.isBuffer(data))
|
|
@@ -24176,21 +24031,37 @@ var init_pages = __esm(() => {
|
|
|
24176
24031
|
ROSTER_PAGE_BYTES = LIMITS.frameBytes - 4096;
|
|
24177
24032
|
PAGE_CACHE_BYTES = 8 * MIB;
|
|
24178
24033
|
});
|
|
24034
|
+
// packages/relay/dist/metrics.js
|
|
24035
|
+
var init_metrics = () => {};
|
|
24036
|
+
|
|
24179
24037
|
// packages/relay/dist/store.js
|
|
24180
24038
|
var init_store3 = __esm(() => {
|
|
24181
24039
|
init_dist();
|
|
24040
|
+
init_metrics();
|
|
24182
24041
|
});
|
|
24183
24042
|
|
|
24043
|
+
// packages/relay/dist/backoff.js
|
|
24044
|
+
function backoffDelay(attempt, minMs, maxMs, random = Math.random) {
|
|
24045
|
+
const ceiling = Math.min(maxMs, minMs * 2 ** Math.min(attempt, 30));
|
|
24046
|
+
return Math.round(ceiling / 2 + random() * (ceiling / 2));
|
|
24047
|
+
}
|
|
24048
|
+
|
|
24184
24049
|
// packages/relay/dist/directory/jelly.js
|
|
24185
24050
|
var init_jelly4 = __esm(() => {
|
|
24186
24051
|
init_dist();
|
|
24187
24052
|
});
|
|
24188
24053
|
|
|
24054
|
+
// packages/relay/dist/directory/witness.js
|
|
24055
|
+
var init_witness = __esm(() => {
|
|
24056
|
+
init_dist();
|
|
24057
|
+
});
|
|
24058
|
+
|
|
24189
24059
|
// packages/relay/dist/directory/directory.js
|
|
24190
24060
|
var WARM_MAX_MS;
|
|
24191
24061
|
var init_directory3 = __esm(() => {
|
|
24192
24062
|
init_dist();
|
|
24193
24063
|
init_jelly4();
|
|
24064
|
+
init_witness();
|
|
24194
24065
|
WARM_MAX_MS = 5 * 60000;
|
|
24195
24066
|
});
|
|
24196
24067
|
|
|
@@ -24199,6 +24070,7 @@ var DAY_MS4 = 86400000, FEED_RETENTION_MS;
|
|
|
24199
24070
|
var init_feed = __esm(() => {
|
|
24200
24071
|
init_dist();
|
|
24201
24072
|
init_jelly4();
|
|
24073
|
+
init_store3();
|
|
24202
24074
|
FEED_RETENTION_MS = 90 * DAY_MS4;
|
|
24203
24075
|
});
|
|
24204
24076
|
|
|
@@ -24231,6 +24103,7 @@ var init_module = __esm(() => {
|
|
|
24231
24103
|
"dir.sth",
|
|
24232
24104
|
"dir.lookup",
|
|
24233
24105
|
"dir.device",
|
|
24106
|
+
"dir.heads",
|
|
24234
24107
|
"dir.resolve",
|
|
24235
24108
|
"dir.search",
|
|
24236
24109
|
"dir.watch",
|
|
@@ -24247,7 +24120,7 @@ var init_store4 = __esm(() => {
|
|
|
24247
24120
|
});
|
|
24248
24121
|
|
|
24249
24122
|
// packages/relay/dist/server.js
|
|
24250
|
-
var HIGH_WATER, MAX_BUFFERED;
|
|
24123
|
+
var FRAME_TYPES, HIGH_WATER, MAX_BUFFERED;
|
|
24251
24124
|
var init_server2 = __esm(() => {
|
|
24252
24125
|
init_dist();
|
|
24253
24126
|
init_frames();
|
|
@@ -24255,20 +24128,43 @@ var init_server2 = __esm(() => {
|
|
|
24255
24128
|
init_limits();
|
|
24256
24129
|
init_pages();
|
|
24257
24130
|
init_store3();
|
|
24131
|
+
init_metrics();
|
|
24258
24132
|
init_module();
|
|
24259
24133
|
init_store4();
|
|
24134
|
+
FRAME_TYPES = new Set([
|
|
24135
|
+
"challenge",
|
|
24136
|
+
"hello",
|
|
24137
|
+
"welcome",
|
|
24138
|
+
"ping",
|
|
24139
|
+
"pong",
|
|
24140
|
+
"join",
|
|
24141
|
+
"roster.append",
|
|
24142
|
+
"roster.sync",
|
|
24143
|
+
"roster",
|
|
24144
|
+
"roster.ok",
|
|
24145
|
+
"peer",
|
|
24146
|
+
"send",
|
|
24147
|
+
"presence",
|
|
24148
|
+
"recv.ack",
|
|
24149
|
+
"deliver",
|
|
24150
|
+
"ack",
|
|
24151
|
+
"nack",
|
|
24152
|
+
"error",
|
|
24153
|
+
"dir.ok",
|
|
24154
|
+
"dir.persons",
|
|
24155
|
+
"dir.headsAre",
|
|
24156
|
+
"dir.sthIs",
|
|
24157
|
+
"dir.resolved",
|
|
24158
|
+
"dir.changed",
|
|
24159
|
+
"dir.unbound",
|
|
24160
|
+
...DIRECTORY_FRAMES
|
|
24161
|
+
]);
|
|
24260
24162
|
HIGH_WATER = 1024 * 1024;
|
|
24261
24163
|
MAX_BUFFERED = 16 * 1024 * 1024;
|
|
24262
24164
|
});
|
|
24263
24165
|
|
|
24264
|
-
// packages/relay/dist/backoff.js
|
|
24265
|
-
function backoffDelay(attempt, minMs, maxMs, random = Math.random) {
|
|
24266
|
-
const ceiling = Math.min(maxMs, minMs * 2 ** Math.min(attempt, 30));
|
|
24267
|
-
return Math.round(ceiling / 2 + random() * (ceiling / 2));
|
|
24268
|
-
}
|
|
24269
|
-
|
|
24270
24166
|
// packages/relay/dist/client.js
|
|
24271
|
-
import { WebSocket
|
|
24167
|
+
import { WebSocket } from "ws";
|
|
24272
24168
|
|
|
24273
24169
|
class RelayClient {
|
|
24274
24170
|
#options;
|
|
@@ -24325,7 +24221,7 @@ class RelayClient {
|
|
|
24325
24221
|
if (!ws)
|
|
24326
24222
|
return;
|
|
24327
24223
|
await new Promise((resolve) => {
|
|
24328
|
-
if (ws.readyState ===
|
|
24224
|
+
if (ws.readyState === WebSocket.CLOSED)
|
|
24329
24225
|
return resolve();
|
|
24330
24226
|
const force = setTimeout(() => ws.terminate(), 2000);
|
|
24331
24227
|
ws.once("close", () => {
|
|
@@ -24347,7 +24243,7 @@ class RelayClient {
|
|
|
24347
24243
|
return existing.promise;
|
|
24348
24244
|
const ws = this.#socket();
|
|
24349
24245
|
if (!ws)
|
|
24350
|
-
return Promise.reject(
|
|
24246
|
+
return Promise.reject(unavailable2("not connected to the relay"));
|
|
24351
24247
|
const text = frameText({ t: "send", env });
|
|
24352
24248
|
if (text instanceof MessagingError)
|
|
24353
24249
|
return Promise.reject(text);
|
|
@@ -24378,6 +24274,9 @@ class RelayClient {
|
|
|
24378
24274
|
dirSth(args = {}) {
|
|
24379
24275
|
return this.#dir("dir.sthIs", { t: "dir.sth", ...args });
|
|
24380
24276
|
}
|
|
24277
|
+
dirHeads(persons, size) {
|
|
24278
|
+
return this.#dir("dir.headsAre", { t: "dir.heads", persons, ...size !== undefined ? { size } : {} });
|
|
24279
|
+
}
|
|
24381
24280
|
dirLookup(persons, size) {
|
|
24382
24281
|
return this.#dir("dir.persons", { t: "dir.lookup", persons, ...size !== undefined ? { size } : {} });
|
|
24383
24282
|
}
|
|
@@ -24407,7 +24306,7 @@ class RelayClient {
|
|
|
24407
24306
|
return Promise.reject(new MessagingError("invalid_request", "this relay link is not to a directory relay"));
|
|
24408
24307
|
const ws = this.#socket();
|
|
24409
24308
|
if (!ws)
|
|
24410
|
-
return Promise.reject(
|
|
24309
|
+
return Promise.reject(unavailable2("not connected to the relay"));
|
|
24411
24310
|
const req = `r${++this.#req}`;
|
|
24412
24311
|
const text = frameText({ ...frame, req });
|
|
24413
24312
|
if (text instanceof MessagingError)
|
|
@@ -24415,7 +24314,7 @@ class RelayClient {
|
|
|
24415
24314
|
const timer = setTimeout(() => {
|
|
24416
24315
|
const request = this.#dirRequests.get(req);
|
|
24417
24316
|
this.#dirRequests.delete(req);
|
|
24418
|
-
request?.pending.reject(
|
|
24317
|
+
request?.pending.reject(unavailable2("relay did not answer in time"));
|
|
24419
24318
|
}, REQUEST_MS);
|
|
24420
24319
|
timer.unref();
|
|
24421
24320
|
const pending = deferred();
|
|
@@ -24447,7 +24346,7 @@ class RelayClient {
|
|
|
24447
24346
|
this.#helloSent = false;
|
|
24448
24347
|
this.#error = undefined;
|
|
24449
24348
|
this.#lastSeen = Date.now();
|
|
24450
|
-
const ws = new
|
|
24349
|
+
const ws = new WebSocket(this.#options.url, SOCKET_OPTIONS);
|
|
24451
24350
|
this.#ws = ws;
|
|
24452
24351
|
this.#welcomeTimer = setTimeout(() => {
|
|
24453
24352
|
if (this.#ws !== ws || this.#connected)
|
|
@@ -24469,7 +24368,7 @@ class RelayClient {
|
|
|
24469
24368
|
clearInterval(this.#pingTimer);
|
|
24470
24369
|
clearTimeout(this.#welcomeTimer);
|
|
24471
24370
|
this.#connected = false;
|
|
24472
|
-
const lost =
|
|
24371
|
+
const lost = unavailable2("relay connection closed");
|
|
24473
24372
|
for (const pending of this.#sends.values())
|
|
24474
24373
|
pending.reject(lost);
|
|
24475
24374
|
for (const request of [...this.#requests.values(), ...this.#dirRequests.values()]) {
|
|
@@ -24658,12 +24557,12 @@ ${id}`;
|
|
|
24658
24557
|
#request(expect, frame) {
|
|
24659
24558
|
const ws = this.#socket();
|
|
24660
24559
|
if (!ws)
|
|
24661
|
-
return Promise.reject(
|
|
24560
|
+
return Promise.reject(unavailable2("not connected to the relay"));
|
|
24662
24561
|
const req = `r${++this.#req}`;
|
|
24663
24562
|
const text = frameText({ ...frame, req });
|
|
24664
24563
|
if (text instanceof MessagingError)
|
|
24665
24564
|
return Promise.reject(text);
|
|
24666
|
-
const timer = setTimeout(() => this.#take(req)?.pending.reject(
|
|
24565
|
+
const timer = setTimeout(() => this.#take(req)?.pending.reject(unavailable2("relay did not answer in time")), REQUEST_MS);
|
|
24667
24566
|
timer.unref();
|
|
24668
24567
|
const request = expect === "roster" ? { expect, pending: deferred(), timer } : { expect, pending: deferred(), timer };
|
|
24669
24568
|
this.#requests.set(req, request);
|
|
@@ -24671,7 +24570,7 @@ ${id}`;
|
|
|
24671
24570
|
return request.pending.promise;
|
|
24672
24571
|
}
|
|
24673
24572
|
#socket() {
|
|
24674
|
-
return this.#connected && this.#ws?.readyState ===
|
|
24573
|
+
return this.#connected && this.#ws?.readyState === WebSocket.OPEN ? this.#ws : undefined;
|
|
24675
24574
|
}
|
|
24676
24575
|
}
|
|
24677
24576
|
function createTeamOnRelay(url, genesis) {
|
|
@@ -24698,7 +24597,7 @@ function directoryOneShot(url, frame, expect) {
|
|
|
24698
24597
|
}
|
|
24699
24598
|
let settled = false;
|
|
24700
24599
|
let sent = false;
|
|
24701
|
-
const ws = new
|
|
24600
|
+
const ws = new WebSocket(url, SOCKET_OPTIONS);
|
|
24702
24601
|
const finish = (error, value) => {
|
|
24703
24602
|
if (settled)
|
|
24704
24603
|
return;
|
|
@@ -24710,7 +24609,7 @@ function directoryOneShot(url, frame, expect) {
|
|
|
24710
24609
|
else
|
|
24711
24610
|
resolve(value);
|
|
24712
24611
|
};
|
|
24713
|
-
const timer = setTimeout(() => finish(
|
|
24612
|
+
const timer = setTimeout(() => finish(unavailable2("relay did not answer")), ONE_SHOT_MS);
|
|
24714
24613
|
ws.on("message", (data, isBinary) => {
|
|
24715
24614
|
if (isBinary)
|
|
24716
24615
|
return;
|
|
@@ -24722,7 +24621,10 @@ function directoryOneShot(url, frame, expect) {
|
|
|
24722
24621
|
}
|
|
24723
24622
|
if (raw?.t === "challenge" && !sent) {
|
|
24724
24623
|
sent = true;
|
|
24725
|
-
|
|
24624
|
+
if (typeof frame === "function" && raw.relay !== relayOrigin(url))
|
|
24625
|
+
return finish(unavailable2("relay id does not match the relay URL origin"));
|
|
24626
|
+
const body = typeof frame === "function" ? frame({ relay: String(raw.relay), nonce: String(raw.nonce) }) : frame;
|
|
24627
|
+
const outgoing = frameText({ ...body, req: "r1" }, LIMITS.preAuthFrameBytes);
|
|
24726
24628
|
if (outgoing instanceof MessagingError)
|
|
24727
24629
|
return finish(outgoing);
|
|
24728
24630
|
ws.send(outgoing);
|
|
@@ -24735,18 +24637,26 @@ function directoryOneShot(url, frame, expect) {
|
|
|
24735
24637
|
}
|
|
24736
24638
|
}
|
|
24737
24639
|
});
|
|
24738
|
-
ws.on("error", (error) => finish(
|
|
24739
|
-
ws.on("close", () => finish(
|
|
24640
|
+
ws.on("error", (error) => finish(unavailable2(`cannot reach the relay: ${error.message}`)));
|
|
24641
|
+
ws.on("close", () => finish(unavailable2("relay closed the connection")));
|
|
24740
24642
|
});
|
|
24741
24643
|
}
|
|
24742
24644
|
function sthOnRelay(url, args = {}) {
|
|
24743
24645
|
return directoryOneShot(url, { t: "dir.sth", ...args }, "dir.sthIs");
|
|
24744
24646
|
}
|
|
24745
|
-
function lookupOnRelay(url, persons, size) {
|
|
24746
|
-
return directoryOneShot(url,
|
|
24647
|
+
function lookupOnRelay(url, persons, size, identity, realm) {
|
|
24648
|
+
return directoryOneShot(url, (challenge) => ({
|
|
24649
|
+
t: "dir.lookup",
|
|
24650
|
+
persons,
|
|
24651
|
+
...size !== undefined ? { size } : {},
|
|
24652
|
+
proof: { device: identity.machine, sig: sign(identity, DOMAIN.dirRead, dirReadPayload({ relay: challenge.relay, realm, device: identity.machine, nonce: challenge.nonce })) }
|
|
24653
|
+
}), "dir.persons");
|
|
24654
|
+
}
|
|
24655
|
+
function headsOnRelay(url, persons, size) {
|
|
24656
|
+
return directoryOneShot(url, { t: "dir.heads", persons, ...size !== undefined ? { size } : {} }, "dir.headsAre");
|
|
24747
24657
|
}
|
|
24748
24658
|
function deviceOnRelay(url, device) {
|
|
24749
|
-
return directoryOneShot(url, { t: "dir.device", device }, "dir.
|
|
24659
|
+
return directoryOneShot(url, { t: "dir.device", device }, "dir.headsAre");
|
|
24750
24660
|
}
|
|
24751
24661
|
function redeemOnRelay(url, entry) {
|
|
24752
24662
|
return oneShot(url, { t: "roster.append", entry }, "roster.ok");
|
|
@@ -24764,7 +24674,7 @@ function fetchRosterAsMember(url, team, identity) {
|
|
|
24764
24674
|
let welcomed = false;
|
|
24765
24675
|
let req = 0;
|
|
24766
24676
|
let page;
|
|
24767
|
-
const ws = new
|
|
24677
|
+
const ws = new WebSocket(url, SOCKET_OPTIONS);
|
|
24768
24678
|
const finish = (error, value) => {
|
|
24769
24679
|
if (settled)
|
|
24770
24680
|
return;
|
|
@@ -24780,7 +24690,7 @@ function fetchRosterAsMember(url, team, identity) {
|
|
|
24780
24690
|
page?.pending.reject(error);
|
|
24781
24691
|
finish(error);
|
|
24782
24692
|
};
|
|
24783
|
-
const timer = setTimeout(() => fail(
|
|
24693
|
+
const timer = setTimeout(() => fail(unavailable2("relay did not answer")), ONE_SHOT_MS);
|
|
24784
24694
|
const fetchPage = (after) => {
|
|
24785
24695
|
const pending = deferred();
|
|
24786
24696
|
const outgoing = frameText({ t: "roster.sync", req: `r${++req}`, after });
|
|
@@ -24829,8 +24739,8 @@ function fetchRosterAsMember(url, team, identity) {
|
|
|
24829
24739
|
fail(new MessagingError(reply.code, reply.message));
|
|
24830
24740
|
}
|
|
24831
24741
|
});
|
|
24832
|
-
ws.on("error", (error) => fail(
|
|
24833
|
-
ws.on("close", () => fail(
|
|
24742
|
+
ws.on("error", (error) => fail(unavailable2(`cannot reach the relay: ${error.message}`)));
|
|
24743
|
+
ws.on("close", () => fail(unavailable2("relay closed the connection")));
|
|
24834
24744
|
});
|
|
24835
24745
|
}
|
|
24836
24746
|
function oneShot(url, frame, expect) {
|
|
@@ -24844,7 +24754,7 @@ function oneShot(url, frame, expect) {
|
|
|
24844
24754
|
const req = "r1";
|
|
24845
24755
|
let settled = false;
|
|
24846
24756
|
let sent = false;
|
|
24847
|
-
const ws = new
|
|
24757
|
+
const ws = new WebSocket(url, SOCKET_OPTIONS);
|
|
24848
24758
|
const finish = (error, value) => {
|
|
24849
24759
|
if (settled)
|
|
24850
24760
|
return;
|
|
@@ -24856,7 +24766,7 @@ function oneShot(url, frame, expect) {
|
|
|
24856
24766
|
else
|
|
24857
24767
|
resolve(value);
|
|
24858
24768
|
};
|
|
24859
|
-
const timer = setTimeout(() => finish(
|
|
24769
|
+
const timer = setTimeout(() => finish(unavailable2("relay did not answer")), ONE_SHOT_MS);
|
|
24860
24770
|
ws.on("message", (data, isBinary) => {
|
|
24861
24771
|
if (isBinary)
|
|
24862
24772
|
return;
|
|
@@ -24893,8 +24803,8 @@ function oneShot(url, frame, expect) {
|
|
|
24893
24803
|
finish(new MessagingError(reply.code, reply.message));
|
|
24894
24804
|
}
|
|
24895
24805
|
});
|
|
24896
|
-
ws.on("error", (error) => finish(
|
|
24897
|
-
ws.on("close", () => finish(
|
|
24806
|
+
ws.on("error", (error) => finish(unavailable2(`cannot reach the relay: ${error.message}`)));
|
|
24807
|
+
ws.on("close", () => finish(unavailable2("relay closed the connection")));
|
|
24898
24808
|
});
|
|
24899
24809
|
}
|
|
24900
24810
|
async function collectPages(after, pages, fetch2) {
|
|
@@ -24930,7 +24840,7 @@ function frameText(frame, limit = LIMITS.frameBytes) {
|
|
|
24930
24840
|
return new MessagingError("too_large", `frame exceeds the relay's ${limit}-byte limit`);
|
|
24931
24841
|
return json;
|
|
24932
24842
|
}
|
|
24933
|
-
function
|
|
24843
|
+
function unavailable2(message) {
|
|
24934
24844
|
return new MessagingError("relay_unavailable", message);
|
|
24935
24845
|
}
|
|
24936
24846
|
function deferred() {
|
|
@@ -24948,12 +24858,12 @@ function callback(fn) {
|
|
|
24948
24858
|
fn();
|
|
24949
24859
|
} catch {}
|
|
24950
24860
|
}
|
|
24951
|
-
var PING_MS = 20000, DEAD_MS, HANDSHAKE_MS = 1e4, WELCOME_MS = 15000, ONE_SHOT_MS = 15000, REQUEST_MS = 15000, MAX_PAGES = 256,
|
|
24861
|
+
var PING_MS = 20000, DEAD_MS, HANDSHAKE_MS = 1e4, WELCOME_MS = 15000, ONE_SHOT_MS = 15000, REQUEST_MS = 15000, MAX_PAGES = 256, SOCKET_OPTIONS, RelayRefusal;
|
|
24952
24862
|
var init_client2 = __esm(() => {
|
|
24953
24863
|
init_dist();
|
|
24954
24864
|
init_frames();
|
|
24955
24865
|
DEAD_MS = 2 * PING_MS + 5000;
|
|
24956
|
-
|
|
24866
|
+
SOCKET_OPTIONS = { maxPayload: LIMITS.frameBytes, perMessageDeflate: false, handshakeTimeout: HANDSHAKE_MS };
|
|
24957
24867
|
RelayRefusal = class RelayRefusal extends MessagingError {
|
|
24958
24868
|
retryAfterMs;
|
|
24959
24869
|
constructor(code, message, retryAfterMs) {
|
|
@@ -25072,6 +24982,9 @@ class RelayDirectoryLink {
|
|
|
25072
24982
|
throw shape();
|
|
25073
24983
|
return { seq: reply.seq, sth: reply.sth };
|
|
25074
24984
|
}
|
|
24985
|
+
async erase(entries, token) {
|
|
24986
|
+
parse5(DirOkFrame, await link((await this.#authed()).dirErase(entries, token)));
|
|
24987
|
+
}
|
|
25075
24988
|
async keys(statement) {
|
|
25076
24989
|
const reply = parse5(DirOkFrame, await link((await this.#authed()).dirKeys(statement)));
|
|
25077
24990
|
if (reply.sth === undefined)
|
|
@@ -25090,11 +25003,21 @@ class RelayDirectoryLink {
|
|
|
25090
25003
|
}
|
|
25091
25004
|
async lookup(persons, size) {
|
|
25092
25005
|
const asks = persons.map((p) => ({ person: p.person, ...p.knownSeq !== undefined ? { knownSeq: p.knownSeq } : {} }));
|
|
25093
|
-
const
|
|
25006
|
+
const { url, identity, realm } = this.#options;
|
|
25007
|
+
const own = this.#options.person();
|
|
25008
|
+
const reply = parse5(DirPersonsFrame, await this.#read((client) => client.dirLookup(asks, size), () => lookupOnRelay(url, asks, size, identity, realm), asks.some((ask) => ask.person !== own)));
|
|
25094
25009
|
return { sth: reply.sth, persons: reply.persons };
|
|
25095
25010
|
}
|
|
25096
|
-
async
|
|
25097
|
-
const
|
|
25011
|
+
async heads(persons, size) {
|
|
25012
|
+
const reply = parse5(DirHeadsAreFrame, await this.#read((client) => client.dirHeads([...persons], size), () => headsOnRelay(this.#options.url, [...persons], size)));
|
|
25013
|
+
return { sth: reply.sth, head: reply.head, persons: reply.persons };
|
|
25014
|
+
}
|
|
25015
|
+
async#read(authed, preAuth, wait = false) {
|
|
25016
|
+
let client = this.#connect();
|
|
25017
|
+
if (wait && client && !client.connected)
|
|
25018
|
+
client = await this.#authed().catch(() => {
|
|
25019
|
+
return;
|
|
25020
|
+
});
|
|
25098
25021
|
if (client?.connected) {
|
|
25099
25022
|
try {
|
|
25100
25023
|
return await authed(client);
|
|
@@ -25106,7 +25029,7 @@ class RelayDirectoryLink {
|
|
|
25106
25029
|
return link(preAuth());
|
|
25107
25030
|
}
|
|
25108
25031
|
async device(device) {
|
|
25109
|
-
const reply = parse5(
|
|
25032
|
+
const reply = parse5(DirHeadsAreFrame, await link(deviceOnRelay(this.#options.url, device)));
|
|
25110
25033
|
return { sth: reply.sth, persons: reply.persons };
|
|
25111
25034
|
}
|
|
25112
25035
|
async resolve(query) {
|
|
@@ -25196,18 +25119,15 @@ var init_relay_transport = __esm(() => {
|
|
|
25196
25119
|
function startDaemon2(options) {
|
|
25197
25120
|
return startDaemon({
|
|
25198
25121
|
...options,
|
|
25199
|
-
transportFactory: options.transportFactory ??
|
|
25122
|
+
transportFactory: options.transportFactory ?? relayTransport,
|
|
25200
25123
|
relayOps: options.relayOps ?? relayOps,
|
|
25201
25124
|
directoryLinkFactory: options.directoryLinkFactory ?? relayDirectoryLink
|
|
25202
25125
|
});
|
|
25203
25126
|
}
|
|
25204
25127
|
var init_dist4 = __esm(() => {
|
|
25205
25128
|
init_daemon();
|
|
25206
|
-
init_direct_transport();
|
|
25207
25129
|
init_relay_transport();
|
|
25208
25130
|
init_relay_transport();
|
|
25209
|
-
init_direct_transport();
|
|
25210
|
-
init_direct_config();
|
|
25211
25131
|
init_home();
|
|
25212
25132
|
init_lock();
|
|
25213
25133
|
init_log();
|
|
@@ -26454,6 +26374,12 @@ var people = {
|
|
|
26454
26374
|
}
|
|
26455
26375
|
ctx.out.line(`${ctx.out.style.bold(inline(person.name, 64))} handle ${inline(person.handle, 64)} org ${person.org} vouched ${person.vouched}`);
|
|
26456
26376
|
ctx.out.lines(table(["DEVICE", "LABEL", ""], person.devices.map((d) => [d.device, inline(d.label, 32), d.self ? "this device" : ""]), ctx.out.style));
|
|
26377
|
+
if (person.deviceChanges !== undefined) {
|
|
26378
|
+
const now = ctx.now();
|
|
26379
|
+
ctx.out.line(`${ctx.out.style.yellow(`${inline(person.name, 64)}'s devices changed:`)}`);
|
|
26380
|
+
for (const change of person.deviceChanges)
|
|
26381
|
+
ctx.out.line(` ${change.change} '${inline(change.label, 32)}' (${vouchWords(change)}) ${ago(change.at, now)}`);
|
|
26382
|
+
}
|
|
26457
26383
|
return;
|
|
26458
26384
|
}
|
|
26459
26385
|
if (ctx.json) {
|
|
@@ -26518,7 +26444,7 @@ var devices = {
|
|
|
26518
26444
|
"directory, removed ones last.",
|
|
26519
26445
|
"link: on a new device Jelly does not enrol, shows its link request and a six-digit code (design \xA72.5).",
|
|
26520
26446
|
"approve: on a device of yours that Jelly bound, shows what the link asks for, then approves it once you type",
|
|
26521
|
-
"the code the new device shows. A device approved by link approves none.
|
|
26447
|
+
"the code the new device shows. A device approved by link approves none. To remove a device: messaging login --remove <device>."
|
|
26522
26448
|
].join(`
|
|
26523
26449
|
`),
|
|
26524
26450
|
options: {
|
|
@@ -26570,8 +26496,9 @@ var notices = {
|
|
|
26570
26496
|
usage: "[--lines] [--ack]",
|
|
26571
26497
|
details: [
|
|
26572
26498
|
"Each line is a message for you (a DM or channel line from someone else, or your own agent's line to you),",
|
|
26573
|
-
"a request held for you to answer (`messaging requests`), or an alert about this device
|
|
26574
|
-
"
|
|
26499
|
+
"a request held for you to answer (`messaging requests`), or an alert: about this device, a device added to or",
|
|
26500
|
+
"removed from your account (with how to remove one that is not yours), or trust you gave that ends within 7 days.",
|
|
26501
|
+
"--lines adds a message's first line (peer text). --ack marks every notice shown as seen."
|
|
26575
26502
|
].join(`
|
|
26576
26503
|
`),
|
|
26577
26504
|
options: {
|
|
@@ -26598,13 +26525,25 @@ var notices = {
|
|
|
26598
26525
|
ago(n.at, now),
|
|
26599
26526
|
n.category,
|
|
26600
26527
|
n.from ? inline(n.from.name, 40) : "",
|
|
26601
|
-
n.conversation !== undefined ? inline(n.conversation, 60) : "",
|
|
26528
|
+
n.conversation !== undefined ? inline(n.conversation, 60) : n.device !== undefined ? `device ${n.device.device}` : "",
|
|
26602
26529
|
n.line !== undefined ? inline(n.line, 120) : ""
|
|
26603
26530
|
]), ctx.out.style));
|
|
26531
|
+
const removable = list.flatMap((n) => n.device?.remove !== undefined && n.device.current ? [n.device] : []);
|
|
26532
|
+
for (const d of removable) {
|
|
26533
|
+
const how = d.remove === "sign-in" ? `messaging login --remove ${d.device}` : d.remove === "team" ? `messaging team remove ${d.device}` : "Remove it from the Jelly app on this device";
|
|
26534
|
+
ctx.out.note(`Not yours? '${inline(d.label, 32)}': ${how}`);
|
|
26535
|
+
}
|
|
26604
26536
|
if (values.ack === true)
|
|
26605
26537
|
ctx.out.line(ctx.out.style.dim(`${list.length} marked seen`));
|
|
26606
26538
|
}
|
|
26607
26539
|
};
|
|
26540
|
+
function vouchWords(d) {
|
|
26541
|
+
if (d.vouch === "admin")
|
|
26542
|
+
return "admitted by the team's admin";
|
|
26543
|
+
if (d.vouch === "device")
|
|
26544
|
+
return d.approver !== undefined ? `approved on '${inline(d.approver, 32)}'` : "approved by link";
|
|
26545
|
+
return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID";
|
|
26546
|
+
}
|
|
26608
26547
|
var DIRECTORY_COMMANDS = [status, who2, people, devices, notices];
|
|
26609
26548
|
|
|
26610
26549
|
// packages/daemon/dist/cli/commands/identity.js
|
|
@@ -26874,14 +26813,62 @@ function aborted2(signal) {
|
|
|
26874
26813
|
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
26875
26814
|
});
|
|
26876
26815
|
}
|
|
26816
|
+
async function showSignIn(ctx, started, open) {
|
|
26817
|
+
if (!ctx.json) {
|
|
26818
|
+
const { bold } = ctx.out.style;
|
|
26819
|
+
const lead = started.purpose === "remove" ? "To remove that device, sign in at Jelly ID:" : started.purpose === "replace" ? "To replace your old device, sign in at Jelly ID with this one:" : started.purpose === "erase" ? "To erase your account, sign in at Jelly ID:" : "To sign this device in, open:";
|
|
26820
|
+
ctx.out.line(lead);
|
|
26821
|
+
ctx.out.line(` ${started.verificationUri}`);
|
|
26822
|
+
ctx.out.line(`and type the code ${bold(started.userCode)} there. Approve only if the page shows this device's key ${bold(started.fingerprint)}.`);
|
|
26823
|
+
}
|
|
26824
|
+
if (open) {
|
|
26825
|
+
const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUri);
|
|
26826
|
+
if (!opened)
|
|
26827
|
+
ctx.out.note("Could not open a browser here; open the page on any device and type the code.");
|
|
26828
|
+
} else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
|
|
26829
|
+
ctx.out.note("Over SSH: open the page on your laptop or phone and type the code; the code ties it to this machine.");
|
|
26830
|
+
}
|
|
26831
|
+
}
|
|
26832
|
+
async function waitSignIn(ctx, client, started) {
|
|
26833
|
+
const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
|
|
26834
|
+
if (!ctx.json)
|
|
26835
|
+
ctx.out.note(`Waiting for your approval (the code expires in ${minutes} min; Ctrl-C cancels)...`);
|
|
26836
|
+
const interrupted = aborted2(ctx.io.signal);
|
|
26837
|
+
let state = { ...started, state: "pending" };
|
|
26838
|
+
while (state.state === "pending") {
|
|
26839
|
+
const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
|
|
26840
|
+
const next = await Promise.race([asked, interrupted.then(() => {
|
|
26841
|
+
return;
|
|
26842
|
+
})]);
|
|
26843
|
+
if (next === undefined) {
|
|
26844
|
+
asked.catch(() => {});
|
|
26845
|
+
await client.call("device.login.cancel", {}).catch(() => {});
|
|
26846
|
+
ctx.out.note("Cancelled.");
|
|
26847
|
+
return;
|
|
26848
|
+
}
|
|
26849
|
+
state = next;
|
|
26850
|
+
}
|
|
26851
|
+
if (state.state === "failed")
|
|
26852
|
+
throw new MessagingError(state.code, state.message);
|
|
26853
|
+
return state;
|
|
26854
|
+
}
|
|
26855
|
+
async function confirmed(ctx, yes, question, word = "y") {
|
|
26856
|
+
if (yes)
|
|
26857
|
+
return true;
|
|
26858
|
+
if (ctx.json)
|
|
26859
|
+
throw new UsageError("with --json there is no prompt: confirm with --yes");
|
|
26860
|
+
ctx.out.prompt(`${question} Type ${word} to confirm: `);
|
|
26861
|
+
const answer = (await readLine(ctx.io.stdin) ?? "").trim().toLowerCase();
|
|
26862
|
+
return word === "y" ? /^y(es)?$/.test(answer) : answer === word;
|
|
26863
|
+
}
|
|
26877
26864
|
var login = {
|
|
26878
26865
|
name: "login",
|
|
26879
26866
|
summary: "Sign this device in to your account through the browser (hosted realm)",
|
|
26880
26867
|
usage: "[--label <label>] [--kind laptop|server|other] [--open] [--no-wait] [--remove <device> [--reason removed|lost]]",
|
|
26881
26868
|
details: [
|
|
26882
|
-
"Prints
|
|
26883
|
-
"check that the page shows the same
|
|
26884
|
-
"device of your account, or one more. Over SSH, open the
|
|
26869
|
+
"Prints Jelly ID's page and a code. Open the page on any device (with --open, this machine's browser opens it),",
|
|
26870
|
+
"type the code, check that the page shows the same key, and approve. This device then binds to your account: the",
|
|
26871
|
+
"first device of your account, or one more. Over SSH, open the page on your laptop or phone.",
|
|
26885
26872
|
"With --remove, the sign-in removes another device of yours instead (a lost one, with --reason lost).",
|
|
26886
26873
|
"Needs Jelly ID's device sign-in; until Jelly ID offers it, this says so and does nothing."
|
|
26887
26874
|
].join(`
|
|
@@ -26889,8 +26876,8 @@ var login = {
|
|
|
26889
26876
|
options: {
|
|
26890
26877
|
label: { type: "string", value: "<label>", description: "This device's label (default: its current one, else device)" },
|
|
26891
26878
|
kind: { type: "string", value: "<kind>", description: "laptop (default), server or other" },
|
|
26892
|
-
open: { type: "boolean", description: "Open
|
|
26893
|
-
"no-wait": { type: "boolean", description: "Print the
|
|
26879
|
+
open: { type: "boolean", description: "Open Jelly ID's page in this machine's browser" },
|
|
26880
|
+
"no-wait": { type: "boolean", description: "Print the page and code, and leave the daemon waiting" },
|
|
26894
26881
|
remove: { type: "string", value: "<device>", description: "Remove that device of yours with this sign-in" },
|
|
26895
26882
|
reason: { type: "string", value: "<reason>", description: "--remove: removed (default) or lost" }
|
|
26896
26883
|
},
|
|
@@ -26917,45 +26904,15 @@ var login = {
|
|
|
26917
26904
|
ctx.out.json(started);
|
|
26918
26905
|
return;
|
|
26919
26906
|
}
|
|
26920
|
-
|
|
26921
|
-
const { bold } = ctx.out.style;
|
|
26922
|
-
ctx.out.line(started.purpose === "remove" ? "To remove that device, sign in at Jelly ID:" : "To sign this device in, open:");
|
|
26923
|
-
ctx.out.line(` ${started.verificationUriComplete}`);
|
|
26924
|
-
ctx.out.line(`and check that the page shows the code ${bold(started.userCode)} and the key ${bold(started.fingerprint)}.`);
|
|
26925
|
-
ctx.out.line(`(Or open ${started.verificationUri} and type the code.)`);
|
|
26926
|
-
}
|
|
26927
|
-
if (values.open === true) {
|
|
26928
|
-
const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUriComplete);
|
|
26929
|
-
if (!opened)
|
|
26930
|
-
ctx.out.note("Could not open a browser here; open the link on any device.");
|
|
26931
|
-
} else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
|
|
26932
|
-
ctx.out.note("Over SSH: open the link on your laptop or phone; the code ties it to this machine.");
|
|
26933
|
-
}
|
|
26907
|
+
await showSignIn(ctx, started, values.open === true);
|
|
26934
26908
|
const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
|
|
26935
26909
|
if (!wait) {
|
|
26936
26910
|
ctx.out.note(`The daemon waits ${minutes} min for your approval. See it with: messaging devices`);
|
|
26937
26911
|
return;
|
|
26938
26912
|
}
|
|
26939
|
-
|
|
26940
|
-
|
|
26941
|
-
|
|
26942
|
-
const interrupted = aborted2(signal);
|
|
26943
|
-
let state = { ...started, state: "pending" };
|
|
26944
|
-
while (state.state === "pending") {
|
|
26945
|
-
const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
|
|
26946
|
-
const next = await Promise.race([asked, interrupted.then(() => {
|
|
26947
|
-
return;
|
|
26948
|
-
})]);
|
|
26949
|
-
if (next === undefined) {
|
|
26950
|
-
asked.catch(() => {});
|
|
26951
|
-
await client.call("device.login.cancel", {}).catch(() => {});
|
|
26952
|
-
ctx.out.note("Cancelled.");
|
|
26953
|
-
return 130;
|
|
26954
|
-
}
|
|
26955
|
-
state = next;
|
|
26956
|
-
}
|
|
26957
|
-
if (state.state === "failed")
|
|
26958
|
-
throw new MessagingError(state.code, state.message);
|
|
26913
|
+
const state = await waitSignIn(ctx, client, started);
|
|
26914
|
+
if (state === undefined)
|
|
26915
|
+
return 130;
|
|
26959
26916
|
if (ctx.json) {
|
|
26960
26917
|
ctx.out.json(state);
|
|
26961
26918
|
return state.state === "done" ? undefined : 1;
|
|
@@ -26972,7 +26929,128 @@ var login = {
|
|
|
26972
26929
|
}
|
|
26973
26930
|
}
|
|
26974
26931
|
};
|
|
26975
|
-
var
|
|
26932
|
+
var replace = {
|
|
26933
|
+
name: "identity replace",
|
|
26934
|
+
summary: "Replace this home's device with new keys after a restore (hosted realm)",
|
|
26935
|
+
usage: "[--label <label>] [--open] [--yes]",
|
|
26936
|
+
details: [
|
|
26937
|
+
"For a home restored from a backup or a snapshot (alarm keys_missing or restored), or for a fresh key on purpose.",
|
|
26938
|
+
"This device gets a new key, binds it to your account and removes the old device (reason replaced): your",
|
|
26939
|
+
"conversations, grants and history stay, and tasks continue. Mail still queued at the relay for the old device is",
|
|
26940
|
+
"lost. Through the browser you approve two sign-ins at Jelly ID: one binds the new key, one removes the old."
|
|
26941
|
+
].join(`
|
|
26942
|
+
`),
|
|
26943
|
+
options: {
|
|
26944
|
+
label: { type: "string", value: "<label>", description: "The new device's label (default: the old one's)" },
|
|
26945
|
+
open: { type: "boolean", description: "Open Jelly ID's page in this machine's browser for each sign-in" },
|
|
26946
|
+
yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
|
|
26947
|
+
},
|
|
26948
|
+
args: { min: 0, max: 0 },
|
|
26949
|
+
async run(ctx, _positionals, values) {
|
|
26950
|
+
const label = stringValue(values, "label");
|
|
26951
|
+
const client = await ctx.client();
|
|
26952
|
+
const preview = await client.call("identity.replace.preview", {});
|
|
26953
|
+
if (!ctx.json)
|
|
26954
|
+
ctx.out.line(inline(preview.sentence, 600));
|
|
26955
|
+
if (!await confirmed(ctx, values.yes === true, `Replace '${inline(preview.predecessor.label, 32)}' (device ${preview.predecessor.device}) with this device?`)) {
|
|
26956
|
+
ctx.out.note("Not replaced.");
|
|
26957
|
+
return 1;
|
|
26958
|
+
}
|
|
26959
|
+
const params = label !== undefined ? { label: parseWith2(MachineLabel, label, "--label must be a lowercase label, e.g. desk") } : {};
|
|
26960
|
+
for (let step = 0;step < 3; step++) {
|
|
26961
|
+
const result = await client.call("identity.replace", params);
|
|
26962
|
+
if (result.state === "done") {
|
|
26963
|
+
if (ctx.json)
|
|
26964
|
+
ctx.out.json(result);
|
|
26965
|
+
else
|
|
26966
|
+
ctx.out.line(`Replaced: this device (${result.device}) replaces ${result.predecessor} on ${inline(result.person, 64)}.`);
|
|
26967
|
+
return;
|
|
26968
|
+
}
|
|
26969
|
+
if (result.state === "token") {
|
|
26970
|
+
if (ctx.json)
|
|
26971
|
+
ctx.out.json(result);
|
|
26972
|
+
else
|
|
26973
|
+
ctx.out.note(`This device (${result.device}) waits for its Jelly host to bind the new key; the host's app finishes the replacement.`);
|
|
26974
|
+
return;
|
|
26975
|
+
}
|
|
26976
|
+
await showSignIn(ctx, result, values.open === true);
|
|
26977
|
+
const state = await waitSignIn(ctx, client, result);
|
|
26978
|
+
if (state === undefined)
|
|
26979
|
+
return 130;
|
|
26980
|
+
if (state.state !== "done") {
|
|
26981
|
+
ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
|
|
26982
|
+
return 1;
|
|
26983
|
+
}
|
|
26984
|
+
if ((await client.call("device.status", {})).binding !== "replace") {
|
|
26985
|
+
const done = { state: "done", person: state.person, device: result.device, predecessor: preview.predecessor.device, seq: state.seq };
|
|
26986
|
+
if (ctx.json)
|
|
26987
|
+
ctx.out.json(done);
|
|
26988
|
+
else
|
|
26989
|
+
ctx.out.line(`Replaced: this device (${result.device}) replaces ${preview.predecessor.device} on ${inline(state.person, 64)}.`);
|
|
26990
|
+
return;
|
|
26991
|
+
}
|
|
26992
|
+
if (!ctx.json)
|
|
26993
|
+
ctx.out.note("The new key is bound. One more sign-in removes the old device.");
|
|
26994
|
+
}
|
|
26995
|
+
throw new MessagingError("internal", "the replacement did not finish; run messaging identity replace again");
|
|
26996
|
+
}
|
|
26997
|
+
};
|
|
26998
|
+
var erase = {
|
|
26999
|
+
name: "erase",
|
|
27000
|
+
summary: "Erase your account from Messaging, for good (hosted realm)",
|
|
27001
|
+
usage: "[--open] [--yes]",
|
|
27002
|
+
details: [
|
|
27003
|
+
"Removes every device of your account and deletes what the directory holds about you. Your Jelly account can",
|
|
27004
|
+
"never be bound to Messaging again. The directory allows 10 erasures per person a day. Shows what is lost and",
|
|
27005
|
+
"asks you to type erase, unless --yes. A device approved by link cannot erase; one signed in through the browser",
|
|
27006
|
+
"approves one more sign-in at Jelly ID."
|
|
27007
|
+
].join(`
|
|
27008
|
+
`),
|
|
27009
|
+
options: {
|
|
27010
|
+
open: { type: "boolean", description: "Open Jelly ID's page in this machine's browser" },
|
|
27011
|
+
yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
|
|
27012
|
+
},
|
|
27013
|
+
args: { min: 0, max: 0 },
|
|
27014
|
+
async run(ctx, _positionals, values) {
|
|
27015
|
+
const client = await ctx.client();
|
|
27016
|
+
const preview = await client.call("directory.erase.preview", {});
|
|
27017
|
+
if (!ctx.json) {
|
|
27018
|
+
ctx.out.line(`Erasing ${inline(preview.person, 64)} from Messaging. What is lost:`);
|
|
27019
|
+
for (const line of preview.lost)
|
|
27020
|
+
ctx.out.line(` - ${inline(line, 300)}`);
|
|
27021
|
+
ctx.out.line(`Devices removed: ${preview.devices.map((d) => `'${inline(d.label, 32)}'${d.self ? " (this one)" : ""}`).join(", ")}`);
|
|
27022
|
+
ctx.out.line("What stays:");
|
|
27023
|
+
for (const line of preview.kept)
|
|
27024
|
+
ctx.out.line(` - ${inline(line, 300)}`);
|
|
27025
|
+
ctx.out.note(`The directory allows ${preview.perDay} erasures per person a day.`);
|
|
27026
|
+
}
|
|
27027
|
+
if (!await confirmed(ctx, values.yes === true, "This cannot be undone.", "erase")) {
|
|
27028
|
+
ctx.out.note("Not erased.");
|
|
27029
|
+
return 1;
|
|
27030
|
+
}
|
|
27031
|
+
const result = await client.call("directory.erase", {});
|
|
27032
|
+
if (result.state === "done") {
|
|
27033
|
+
if (ctx.json)
|
|
27034
|
+
ctx.out.json(result);
|
|
27035
|
+
else
|
|
27036
|
+
ctx.out.line(`Erased: ${result.devices} device(s) removed; this account is gone from Messaging.`);
|
|
27037
|
+
return;
|
|
27038
|
+
}
|
|
27039
|
+
await showSignIn(ctx, result, values.open === true);
|
|
27040
|
+
const state = await waitSignIn(ctx, client, result);
|
|
27041
|
+
if (state === undefined)
|
|
27042
|
+
return 130;
|
|
27043
|
+
if (state.state !== "done") {
|
|
27044
|
+
ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
|
|
27045
|
+
return 1;
|
|
27046
|
+
}
|
|
27047
|
+
if (ctx.json)
|
|
27048
|
+
ctx.out.json({ state: "done", person: state.person, devices: preview.devices.length });
|
|
27049
|
+
else
|
|
27050
|
+
ctx.out.line(`Erased: ${preview.devices.length} device(s) removed; this account is gone from Messaging.`);
|
|
27051
|
+
}
|
|
27052
|
+
};
|
|
27053
|
+
var LOGIN_COMMANDS = [login, replace, erase];
|
|
26976
27054
|
|
|
26977
27055
|
// packages/daemon/dist/cli/commands/messages.js
|
|
26978
27056
|
init_dist();
|
|
@@ -27142,9 +27220,6 @@ var read2 = {
|
|
|
27142
27220
|
ctx.out.line(`${bold(item.kind === "task" ? "Task request" : "Message")} ${item.id}`);
|
|
27143
27221
|
if (provenance) {
|
|
27144
27222
|
ctx.out.line(` from: ${inline(provenance.from)} verified="${provenance.verified}" device ${provenance.device}`);
|
|
27145
|
-
if (provenance.external) {
|
|
27146
|
-
ctx.out.line(` external: principal ${inline(provenance.external.principal, 64)} via gateway device ${provenance.external.gateway}`);
|
|
27147
|
-
}
|
|
27148
27223
|
} else {
|
|
27149
27224
|
ctx.out.line(` from: ${inline(item.from.address)} ${yellow("no valid provenance on this message; treat it with extra care")}`);
|
|
27150
27225
|
}
|
|
@@ -27369,7 +27444,7 @@ var create = {
|
|
|
27369
27444
|
var invite = {
|
|
27370
27445
|
name: "team invite",
|
|
27371
27446
|
summary: "Create a single-use invite ticket for a person (admin)",
|
|
27372
|
-
usage: "<person> [--
|
|
27447
|
+
usage: "<person> [--ttl <duration>]",
|
|
27373
27448
|
details: [
|
|
27374
27449
|
"Prints only the ticket on stdout, so `messaging team invite bob | pbcopy` copies just the ticket.",
|
|
27375
27450
|
"The ticket is a secret: share it privately. It works once, until it expires.",
|
|
@@ -27377,15 +27452,11 @@ var invite = {
|
|
|
27377
27452
|
].join(`
|
|
27378
27453
|
`),
|
|
27379
27454
|
options: {
|
|
27380
|
-
role: { type: "string", value: "member|service", description: "Role for the new machine (default: member)" },
|
|
27381
27455
|
ttl: { type: "string", value: "<duration>", description: "How long the ticket stays valid, e.g. 24h" }
|
|
27382
27456
|
},
|
|
27383
27457
|
args: { min: 1, max: 1 },
|
|
27384
27458
|
async run(ctx, positionals, values) {
|
|
27385
27459
|
const person = parseWith2(PersonName, positionals[0], "person must be a lowercase person name, e.g. bob");
|
|
27386
|
-
const roleText = stringValue(values, "role");
|
|
27387
|
-
if (roleText !== undefined && roleText !== "member" && roleText !== "service")
|
|
27388
|
-
throw new UsageError("--role must be member or service");
|
|
27389
27460
|
const ttlText = stringValue(values, "ttl");
|
|
27390
27461
|
let ttlMs;
|
|
27391
27462
|
if (ttlText !== undefined) {
|
|
@@ -27396,13 +27467,12 @@ var invite = {
|
|
|
27396
27467
|
const client = await ctx.client();
|
|
27397
27468
|
const result = await client.call("team.invite", {
|
|
27398
27469
|
person,
|
|
27399
|
-
...roleText !== undefined ? { role: roleText } : {},
|
|
27400
27470
|
...ttlMs !== undefined ? { ttlMs } : {}
|
|
27401
27471
|
});
|
|
27402
27472
|
if (ctx.json) {
|
|
27403
27473
|
ctx.out.json(result);
|
|
27404
27474
|
} else {
|
|
27405
|
-
ctx.out.note(`Invite ${result.inviteId} for ${person}
|
|
27475
|
+
ctx.out.note(`Invite ${result.inviteId} for ${person}, expires ${iso2(result.expires)} (${until(result.expires, ctx.now())}).`);
|
|
27406
27476
|
ctx.out.line(result.ticket);
|
|
27407
27477
|
}
|
|
27408
27478
|
ctx.out.warn(`this ticket is a secret. Share it privately with ${person}, never in a public channel or a shared log.`);
|
|
@@ -27461,7 +27531,7 @@ var members2 = {
|
|
|
27461
27531
|
const now = ctx.now();
|
|
27462
27532
|
ctx.out.line();
|
|
27463
27533
|
ctx.out.line(`Invites (${result.invites.length})`);
|
|
27464
|
-
ctx.out.lines(table(["INVITE", "PERSON", "
|
|
27534
|
+
ctx.out.lines(table(["INVITE", "PERSON", "STATE", "EXPIRES"], result.invites.map((i) => [i.inviteId, i.person, i.state, i.state === "open" ? until(i.expires, now) : iso2(i.expires)]), style));
|
|
27465
27535
|
}
|
|
27466
27536
|
}
|
|
27467
27537
|
};
|
|
@@ -27523,10 +27593,10 @@ var remove = {
|
|
|
27523
27593
|
var role = {
|
|
27524
27594
|
name: "team role",
|
|
27525
27595
|
summary: "Change a member's role (admin)",
|
|
27526
|
-
usage: "<machine|person/label> <admin|member
|
|
27596
|
+
usage: "<machine|person/label> <admin|member>",
|
|
27527
27597
|
args: { min: 2, max: 2 },
|
|
27528
27598
|
async run(ctx, positionals) {
|
|
27529
|
-
const newRole = parseWith2(MemberRole, positionals[1], "role must be admin
|
|
27599
|
+
const newRole = parseWith2(MemberRole, positionals[1], "role must be admin or member");
|
|
27530
27600
|
const { member } = await resolveMember(ctx, positionals[0]);
|
|
27531
27601
|
const client = await ctx.client();
|
|
27532
27602
|
const result = await client.call("team.role", { machine: member.machine, role: newRole });
|
|
@@ -27574,17 +27644,13 @@ var card2 = {
|
|
|
27574
27644
|
var add = {
|
|
27575
27645
|
name: "team add",
|
|
27576
27646
|
summary: "Admit a machine by its public admit card (admin)",
|
|
27577
|
-
usage: "<card|-> [--
|
|
27647
|
+
usage: "<card|-> [--yes]",
|
|
27578
27648
|
details: "Pass `-` to read the card from stdin (then pass --yes). The card has public keys only. You are admitting that exact machine. Asks for confirmation unless --yes.",
|
|
27579
27649
|
options: {
|
|
27580
|
-
role: { type: "string", value: "member|service", description: "Role for the new machine (default: member)" },
|
|
27581
27650
|
yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
|
|
27582
27651
|
},
|
|
27583
27652
|
args: { min: 1, max: 1 },
|
|
27584
27653
|
async run(ctx, positionals, values) {
|
|
27585
|
-
const roleText = stringValue(values, "role");
|
|
27586
|
-
if (roleText !== undefined && roleText !== "member" && roleText !== "service")
|
|
27587
|
-
throw new UsageError("--role must be member or service");
|
|
27588
27654
|
const fromStdin = positionals[0] === "-";
|
|
27589
27655
|
const text = (fromStdin ? await readAll(ctx.io.stdin, 8192) : positionals[0]).trim();
|
|
27590
27656
|
const card = decodeAdmitCard(text);
|
|
@@ -27601,8 +27667,7 @@ var add = {
|
|
|
27601
27667
|
machine: card.machine,
|
|
27602
27668
|
enc: card.enc,
|
|
27603
27669
|
person: card.person,
|
|
27604
|
-
label: card.label
|
|
27605
|
-
...roleText !== undefined ? { role: roleText } : {}
|
|
27670
|
+
label: card.label
|
|
27606
27671
|
});
|
|
27607
27672
|
if (ctx.json)
|
|
27608
27673
|
ctx.out.json({ ok: true, person: card.person, label: card.label, machine: card.machine });
|
|
@@ -27724,13 +27789,13 @@ var HANDLES_INTERRUPT = new Set(["listen", "login"]);
|
|
|
27724
27789
|
var GROUPS = {
|
|
27725
27790
|
daemon: "Run the local daemon, messagingd",
|
|
27726
27791
|
team: "Manage the team: create, invite, join, members, roles (only the CLI may do this)",
|
|
27727
|
-
identity: "Show, move or
|
|
27792
|
+
identity: "Show, move, rotate or replace this machine's keys (only the CLI may do this)",
|
|
27728
27793
|
chat: "Your DMs and channels: list, read, post, search"
|
|
27729
27794
|
};
|
|
27730
27795
|
var SECTIONS = [
|
|
27731
27796
|
["Daemon", ["daemon start", "daemon stop", "status"]],
|
|
27732
27797
|
["Team", ["team prepare", "team create", "team card", "team invite", "team join", "team pair", "team add", "team claim", "team pair-cancel", "team members", "team remove", "team role", "team revoke-invite"]],
|
|
27733
|
-
["Identity", ["identity show", "identity migrate", "identity rotate-enc"]],
|
|
27798
|
+
["Identity", ["identity show", "identity migrate", "identity rotate-enc", "identity replace", "erase"]],
|
|
27734
27799
|
["People", ["login", "people", "devices", "grants", "grant", "requests", "contact", "card", "notices"]],
|
|
27735
27800
|
["Messages", ["who", "send", "inbox", "read", "message-status"]],
|
|
27736
27801
|
["Chats", ["chat list", "chat read", "chat post", "chat search"]],
|
|
@@ -27964,6 +28029,37 @@ function cliMain(argv, start) {
|
|
|
27964
28029
|
// packages/daemon/dist/bin/daemon-main.js
|
|
27965
28030
|
init_dist2();
|
|
27966
28031
|
import { resolve as resolve4 } from "path";
|
|
28032
|
+
|
|
28033
|
+
// packages/daemon/dist/bin/fatal.js
|
|
28034
|
+
import { writeSync as writeSync2 } from "fs";
|
|
28035
|
+
var sink;
|
|
28036
|
+
function setFatalLog(log) {
|
|
28037
|
+
sink = log;
|
|
28038
|
+
}
|
|
28039
|
+
function installFatalPolicy(name) {
|
|
28040
|
+
const fatal = (origin) => (error) => {
|
|
28041
|
+
const fields = { origin, error: error instanceof Error ? error.name : typeof error, ...frameOf(error) };
|
|
28042
|
+
try {
|
|
28043
|
+
sink?.("error", "daemon.fatal", fields);
|
|
28044
|
+
} catch {}
|
|
28045
|
+
const frames = error instanceof Error ? error.stack?.split(`
|
|
28046
|
+
`).slice(1).join(`
|
|
28047
|
+
`) ?? "" : "";
|
|
28048
|
+
writeSync2(2, `${name}: fatal ${fields.origin}: ${fields.error}
|
|
28049
|
+
${frames}
|
|
28050
|
+
`);
|
|
28051
|
+
process.exit(70);
|
|
28052
|
+
};
|
|
28053
|
+
process.on("uncaughtException", fatal("uncaughtException"));
|
|
28054
|
+
process.on("unhandledRejection", fatal("unhandledRejection"));
|
|
28055
|
+
}
|
|
28056
|
+
function frameOf(error) {
|
|
28057
|
+
const frame = error instanceof Error ? error.stack?.split(`
|
|
28058
|
+
`).find((line) => line.trimStart().startsWith("at ")) : undefined;
|
|
28059
|
+
return frame ? { at: frame.trim().slice(3, 240) } : {};
|
|
28060
|
+
}
|
|
28061
|
+
|
|
28062
|
+
// packages/daemon/dist/bin/daemon-main.js
|
|
27967
28063
|
var USAGE = `usage: messagingd [--home <dir>]
|
|
27968
28064
|
messagingd --deployment jelly --home <dir> --act-key <path> --label <label>
|
|
27969
28065
|
[--kind jelly-host] [--run <dir>] [--socket-group <gid>]
|
|
@@ -28094,6 +28190,7 @@ ${USAGE}`);
|
|
|
28094
28190
|
stderr: process.stderr.isTTY === true,
|
|
28095
28191
|
level: level === "debug" || level === "warn" || level === "error" ? level : "info"
|
|
28096
28192
|
});
|
|
28193
|
+
setFatalLog(log);
|
|
28097
28194
|
let daemon;
|
|
28098
28195
|
try {
|
|
28099
28196
|
const jellyHost = fromEnv ? {
|
|
@@ -28164,6 +28261,8 @@ function filterSqliteWarning() {
|
|
|
28164
28261
|
// packages/daemon/dist/bin/standalone.js
|
|
28165
28262
|
filterSqliteWarning();
|
|
28166
28263
|
var { mode, args } = dispatch(process.argv0, process.argv.slice(2));
|
|
28264
|
+
if (mode !== "cli")
|
|
28265
|
+
installFatalPolicy("messagingd");
|
|
28167
28266
|
var done = mode === "cli" ? cliMain(args, { daemonCommand: [process.execPath] }) : daemonMain(args);
|
|
28168
28267
|
done.then((code) => {
|
|
28169
28268
|
process.exitCode = code;
|