@joe-broadhead/messaging-bin-win32-x64 0.4.0 → 0.6.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 +1749 -99
- package/package.json +1 -1
package/lib/messagingd.mjs
CHANGED
|
@@ -6391,7 +6391,8 @@ var init_jelly = __esm(() => {
|
|
|
6391
6391
|
jti: string2().regex(/^[\x21-\x7e]{1,128}$/, "printable jti"),
|
|
6392
6392
|
iss: string2().max(2048).optional(),
|
|
6393
6393
|
dev: string2().max(200).optional(),
|
|
6394
|
-
cls: string2().max(32).optional()
|
|
6394
|
+
cls: string2().max(32).optional(),
|
|
6395
|
+
mbr: string2().max(200).optional()
|
|
6395
6396
|
});
|
|
6396
6397
|
JellySessionClaims = looseObject({
|
|
6397
6398
|
aud: string2().min(1).max(2048),
|
|
@@ -6900,7 +6901,8 @@ var init_envelope = __esm(() => {
|
|
|
6900
6901
|
label: MachineLabel,
|
|
6901
6902
|
sessions: array(SessionInfo).max(LIMITS.presenceSessions),
|
|
6902
6903
|
features: array(string2().max(64)).max(32).optional(),
|
|
6903
|
-
direct: array(DirectEndpoint).min(1).max(4).optional()
|
|
6904
|
+
direct: array(DirectEndpoint).min(1).max(4).optional(),
|
|
6905
|
+
away: literal(true).optional()
|
|
6904
6906
|
});
|
|
6905
6907
|
PAYLOAD_BY_TYPE = {
|
|
6906
6908
|
message: MessagePayload,
|
|
@@ -7102,7 +7104,7 @@ var init_relay = __esm(() => {
|
|
|
7102
7104
|
function hostedRelayAuthPayload(args) {
|
|
7103
7105
|
return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, person: args.person, device: args.device, nonce: args.nonce, ts: args.ts };
|
|
7104
7106
|
}
|
|
7105
|
-
var Req2, Size, Handle, Email, OrgId, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, DirectoryClientFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
|
|
7107
|
+
var Req2, Size, Handle, Email, OrgId, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, 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, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
|
|
7106
7108
|
var init_directory_frames = __esm(() => {
|
|
7107
7109
|
init_zod();
|
|
7108
7110
|
init_common();
|
|
@@ -7167,6 +7169,17 @@ var init_directory_frames = __esm(() => {
|
|
|
7167
7169
|
entries: array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
|
|
7168
7170
|
token: Jws
|
|
7169
7171
|
});
|
|
7172
|
+
WAKE_TICKET_MAX_S = 7 * 24 * 60 * 60;
|
|
7173
|
+
WakeUrl = string2().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts\/hst_[A-Za-z0-9._:-]{1,123}\/wake$/, "a Jelly ID wake URL");
|
|
7174
|
+
WakeArmFrame = object({
|
|
7175
|
+
t: literal("wake.arm"),
|
|
7176
|
+
req: Req2,
|
|
7177
|
+
url: WakeUrl,
|
|
7178
|
+
ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
|
|
7179
|
+
agents: array(SessionName).max(WAKE_AGENTS),
|
|
7180
|
+
awaiting: array(Ulid).max(WAKE_AWAITING)
|
|
7181
|
+
});
|
|
7182
|
+
WakeDisarmFrame = object({ t: literal("wake.disarm"), req: Req2 });
|
|
7170
7183
|
DirectoryClientFrame = discriminatedUnion("t", [
|
|
7171
7184
|
DirBindFrame,
|
|
7172
7185
|
HostedHelloFrame,
|
|
@@ -7179,7 +7192,9 @@ var init_directory_frames = __esm(() => {
|
|
|
7179
7192
|
DirSearchFrame,
|
|
7180
7193
|
DirWatchFrame,
|
|
7181
7194
|
PresenceWatchFrame,
|
|
7182
|
-
DirEraseFrame
|
|
7195
|
+
DirEraseFrame,
|
|
7196
|
+
WakeArmFrame,
|
|
7197
|
+
WakeDisarmFrame
|
|
7183
7198
|
]);
|
|
7184
7199
|
HostedWelcomeFrame = object({ t: literal("welcome"), sth: STH });
|
|
7185
7200
|
DirOkFrame = object({
|
|
@@ -7261,14 +7276,14 @@ var init_direct = __esm(() => {
|
|
|
7261
7276
|
});
|
|
7262
7277
|
|
|
7263
7278
|
// packages/core/dist/schema/ipc/device.js
|
|
7264
|
-
var BindingState, RealmAlarmSummary, DeviceStatus, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DEVICE_METHODS;
|
|
7279
|
+
var BindingState, RealmAlarmSummary, DeviceStatus, DeviceChangeView, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, IdentityReplaceParams, SignInStep, DirectoryEraseParams, DEVICE_METHODS;
|
|
7265
7280
|
var init_device = __esm(() => {
|
|
7266
7281
|
init_zod();
|
|
7267
7282
|
init_common();
|
|
7268
7283
|
init_directory();
|
|
7269
7284
|
init_errors3();
|
|
7270
7285
|
init_jelly();
|
|
7271
|
-
BindingState = _enum(["none", "bound", "attest-due", "removed"]);
|
|
7286
|
+
BindingState = _enum(["none", "bound", "attest-due", "removed", "replace"]);
|
|
7272
7287
|
RealmAlarmSummary = object({
|
|
7273
7288
|
kind: _enum(["directory", "restored"]),
|
|
7274
7289
|
reason: string2().max(500),
|
|
@@ -7283,6 +7298,16 @@ var init_device = __esm(() => {
|
|
|
7283
7298
|
sth: object({ size: number2().int().positive(), root: Hash, ts: Millis }).optional(),
|
|
7284
7299
|
alarm: RealmAlarmSummary.optional()
|
|
7285
7300
|
});
|
|
7301
|
+
DeviceChangeView = object({
|
|
7302
|
+
change: _enum(["added", "vouched", "removed", "replaced"]),
|
|
7303
|
+
device: MachineId,
|
|
7304
|
+
label: MachineLabel,
|
|
7305
|
+
kind: DeviceKind.optional(),
|
|
7306
|
+
vouch: _enum(["jelly", "device", "admin"]),
|
|
7307
|
+
via: _enum(["host", "browser"]).optional(),
|
|
7308
|
+
approver: MachineLabel.optional(),
|
|
7309
|
+
at: Millis
|
|
7310
|
+
});
|
|
7286
7311
|
DeviceListEntry = object({
|
|
7287
7312
|
device: MachineId,
|
|
7288
7313
|
label: MachineLabel,
|
|
@@ -7299,7 +7324,7 @@ var init_device = __esm(() => {
|
|
|
7299
7324
|
LinkPreview = object({ device: MachineId, label: MachineLabel, kind: DeviceKind, sas: Sas });
|
|
7300
7325
|
KeyFingerprint = string2().regex(/^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$/, "key fingerprint");
|
|
7301
7326
|
LoginUserCode = string2().regex(/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/, "user code");
|
|
7302
|
-
LoginPurpose = _enum(["bind", "remove"]);
|
|
7327
|
+
LoginPurpose = _enum(["bind", "remove", "replace", "erase"]);
|
|
7303
7328
|
LoginKind = _enum(["laptop", "server", "other"]);
|
|
7304
7329
|
LoginShown = object({
|
|
7305
7330
|
verificationUriComplete: string2().url().max(2048),
|
|
@@ -7315,6 +7340,16 @@ var init_device = __esm(() => {
|
|
|
7315
7340
|
object({ state: literal("failed"), purpose: LoginPurpose, code: ErrorCode, message: string2().max(500) }),
|
|
7316
7341
|
object({ state: literal("cancelled"), purpose: LoginPurpose })
|
|
7317
7342
|
]);
|
|
7343
|
+
DeviceRemoveParams = strictObject({ device: MachineId, reason: _enum(["self", "removed", "lost"]).optional(), token: Jws.optional() });
|
|
7344
|
+
DeviceApproveParams = strictObject({ link: LinkText, sas: Sas });
|
|
7345
|
+
IdentityReplaceParams = strictObject({
|
|
7346
|
+
token: Jws.optional(),
|
|
7347
|
+
label: MachineLabel.optional()
|
|
7348
|
+
});
|
|
7349
|
+
SignInStep = LoginShown.extend({ state: literal("sign-in"), purpose: LoginPurpose, interval: number2().int().positive() });
|
|
7350
|
+
DirectoryEraseParams = strictObject({
|
|
7351
|
+
token: Jws.optional()
|
|
7352
|
+
});
|
|
7318
7353
|
DEVICE_METHODS = {
|
|
7319
7354
|
"device.status": { params: strictObject({}), result: DeviceStatus },
|
|
7320
7355
|
"device.bind": {
|
|
@@ -7323,7 +7358,7 @@ var init_device = __esm(() => {
|
|
|
7323
7358
|
},
|
|
7324
7359
|
"device.list": { params: strictObject({}), result: object({ devices: array(DeviceListEntry) }) },
|
|
7325
7360
|
"device.remove": {
|
|
7326
|
-
params:
|
|
7361
|
+
params: DeviceRemoveParams,
|
|
7327
7362
|
result: object({ seq: number2().int().nonnegative() })
|
|
7328
7363
|
},
|
|
7329
7364
|
"device.link": {
|
|
@@ -7340,10 +7375,51 @@ var init_device = __esm(() => {
|
|
|
7340
7375
|
},
|
|
7341
7376
|
"device.login.status": { params: strictObject({ waitMs: number2().int().min(0).max(120000).optional() }), result: LoginState },
|
|
7342
7377
|
"device.login.cancel": { params: strictObject({}), result: object({ ok: literal(true) }) },
|
|
7343
|
-
"device.approve.preview": { params:
|
|
7378
|
+
"device.approve.preview": { params: DeviceApproveParams.partial({ sas: true }), result: LinkPreview },
|
|
7344
7379
|
"device.approve": {
|
|
7345
|
-
params:
|
|
7380
|
+
params: DeviceApproveParams,
|
|
7346
7381
|
result: object({ device: MachineId, seq: number2().int().nonnegative() })
|
|
7382
|
+
},
|
|
7383
|
+
"identity.replace": {
|
|
7384
|
+
params: IdentityReplaceParams,
|
|
7385
|
+
result: discriminatedUnion("state", [
|
|
7386
|
+
object({ state: literal("done"), person: PersonId, device: MachineId, predecessor: MachineId, seq: number2().int().nonnegative() }),
|
|
7387
|
+
object({ state: literal("token"), device: MachineId }),
|
|
7388
|
+
SignInStep.extend({ device: MachineId })
|
|
7389
|
+
])
|
|
7390
|
+
},
|
|
7391
|
+
"identity.replace.preview": {
|
|
7392
|
+
params: IdentityReplaceParams,
|
|
7393
|
+
result: object({
|
|
7394
|
+
predecessor: object({
|
|
7395
|
+
device: MachineId,
|
|
7396
|
+
label: MachineLabel,
|
|
7397
|
+
kind: DeviceKind.optional(),
|
|
7398
|
+
vouch: _enum(["jelly", "device"]),
|
|
7399
|
+
via: _enum(["host", "browser"]).optional(),
|
|
7400
|
+
since: Millis.optional(),
|
|
7401
|
+
current: boolean2()
|
|
7402
|
+
}),
|
|
7403
|
+
alarm: _enum(["keys_missing", "restored"]).nullable(),
|
|
7404
|
+
device: MachineId.optional(),
|
|
7405
|
+
signIns: number2().int().min(0).max(2),
|
|
7406
|
+
sentence: string2().max(600)
|
|
7407
|
+
})
|
|
7408
|
+
},
|
|
7409
|
+
"directory.erase": {
|
|
7410
|
+
params: DirectoryEraseParams,
|
|
7411
|
+
result: discriminatedUnion("state", [object({ state: literal("done"), person: PersonId, devices: number2().int().nonnegative() }), SignInStep])
|
|
7412
|
+
},
|
|
7413
|
+
"directory.erase.preview": {
|
|
7414
|
+
params: DirectoryEraseParams,
|
|
7415
|
+
result: object({
|
|
7416
|
+
person: PersonId,
|
|
7417
|
+
devices: array(object({ device: MachineId, label: MachineLabel, kind: DeviceKind.optional(), self: boolean2() })),
|
|
7418
|
+
signIns: number2().int().min(0).max(1),
|
|
7419
|
+
perDay: number2().int().positive(),
|
|
7420
|
+
lost: array(string2().max(300)).max(10),
|
|
7421
|
+
kept: array(string2().max(300)).max(10)
|
|
7422
|
+
})
|
|
7347
7423
|
}
|
|
7348
7424
|
};
|
|
7349
7425
|
});
|
|
@@ -7458,7 +7534,7 @@ var init_shapes = __esm(() => {
|
|
|
7458
7534
|
});
|
|
7459
7535
|
|
|
7460
7536
|
// packages/core/dist/schema/ipc/consent.js
|
|
7461
|
-
var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, CONSENT_METHODS;
|
|
7537
|
+
var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, RequestAnswerParams, ContactRequestParams, CONSENT_METHODS;
|
|
7462
7538
|
var init_consent2 = __esm(() => {
|
|
7463
7539
|
init_zod();
|
|
7464
7540
|
init_common();
|
|
@@ -7524,7 +7600,9 @@ var init_consent2 = __esm(() => {
|
|
|
7524
7600
|
HeldRequest = union([AskRequest, ContactRequestView, ContactSuggestion]);
|
|
7525
7601
|
RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
|
|
7526
7602
|
RequestText = string2().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
|
|
7527
|
-
PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional() });
|
|
7603
|
+
PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional(), devicesChangedAt: Millis.optional() });
|
|
7604
|
+
RequestAnswerParams = strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() });
|
|
7605
|
+
ContactRequestParams = strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() });
|
|
7528
7606
|
CONSENT_METHODS = {
|
|
7529
7607
|
"grant.list": {
|
|
7530
7608
|
params: strictObject({ subject: GrantTarget.optional() }),
|
|
@@ -7534,14 +7612,25 @@ var init_consent2 = __esm(() => {
|
|
|
7534
7612
|
"grant.set.preview": { params: GrantSetParams, result: object({ sentence: string2().max(500) }) },
|
|
7535
7613
|
"request.list": { params: strictObject({}), result: object({ requests: array(HeldRequest).max(LIMITS.heldTotal * 3) }) },
|
|
7536
7614
|
"request.answer": {
|
|
7537
|
-
params:
|
|
7615
|
+
params: RequestAnswerParams,
|
|
7538
7616
|
result: object({ answered: array(Ulid).max(LIMITS.heldTotal) })
|
|
7539
7617
|
},
|
|
7618
|
+
"request.answer.preview": {
|
|
7619
|
+
params: RequestAnswerParams,
|
|
7620
|
+
result: object({
|
|
7621
|
+
kind: _enum(["message", "task", "contact", "suggestion"]),
|
|
7622
|
+
from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }),
|
|
7623
|
+
agent: SessionName.optional(),
|
|
7624
|
+
to: SessionName.optional(),
|
|
7625
|
+
count: number2().int().positive(),
|
|
7626
|
+
sentence: string2().max(500)
|
|
7627
|
+
})
|
|
7628
|
+
},
|
|
7540
7629
|
"contact.request": {
|
|
7541
|
-
params:
|
|
7630
|
+
params: ContactRequestParams,
|
|
7542
7631
|
result: object({ id: Ulid, conversation: RoomRef, person: PersonId })
|
|
7543
7632
|
},
|
|
7544
|
-
"contact.request.preview": { params:
|
|
7633
|
+
"contact.request.preview": { params: ContactRequestParams.partial({ text: true }), result: object({ found: boolean2() }) },
|
|
7545
7634
|
"card.get": { params: strictObject({ person: PersonId }), result: object({ sent: boolean2() }) }
|
|
7546
7635
|
};
|
|
7547
7636
|
});
|
|
@@ -7556,6 +7645,7 @@ var init_surfaces = __esm(() => {
|
|
|
7556
7645
|
init_directory_frames();
|
|
7557
7646
|
init_errors3();
|
|
7558
7647
|
init_consent2();
|
|
7648
|
+
init_device();
|
|
7559
7649
|
init_shapes();
|
|
7560
7650
|
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>");
|
|
7561
7651
|
ConvPlace = union([ChatSeq, ConvCursor]);
|
|
@@ -7660,26 +7750,38 @@ var init_surfaces = __esm(() => {
|
|
|
7660
7750
|
},
|
|
7661
7751
|
"people.get": {
|
|
7662
7752
|
params: strictObject({ person: PersonId }),
|
|
7663
|
-
result: PersonSummary.extend({
|
|
7753
|
+
result: PersonSummary.extend({
|
|
7754
|
+
devices: array(object({ device: MachineId, label: MachineLabel, self: boolean2(), away: literal(true).optional() })),
|
|
7755
|
+
deviceChanges: array(DeviceChangeView).max(10).optional()
|
|
7756
|
+
})
|
|
7664
7757
|
}
|
|
7665
7758
|
};
|
|
7666
7759
|
});
|
|
7667
7760
|
|
|
7668
7761
|
// packages/core/dist/schema/ipc/jelly.js
|
|
7669
|
-
var NoticeId, NOTICE_LINE_CHARS = 120, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
|
|
7762
|
+
var NoticeId, NOTICE_LINE_CHARS = 120, NoticeDevice, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
|
|
7670
7763
|
var init_jelly2 = __esm(() => {
|
|
7671
7764
|
init_zod();
|
|
7672
7765
|
init_common();
|
|
7766
|
+
init_directory_frames();
|
|
7767
|
+
init_jelly();
|
|
7673
7768
|
init_directory();
|
|
7769
|
+
init_device();
|
|
7674
7770
|
init_shapes();
|
|
7675
7771
|
init_surfaces();
|
|
7676
7772
|
NoticeId = string2().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
|
|
7773
|
+
NoticeDevice = DeviceChangeView.extend({
|
|
7774
|
+
replaces: object({ device: MachineId, label: MachineLabel }).optional(),
|
|
7775
|
+
current: boolean2(),
|
|
7776
|
+
remove: _enum(["token", "sign-in", "team"]).optional()
|
|
7777
|
+
});
|
|
7677
7778
|
Notice = object({
|
|
7678
7779
|
id: NoticeId,
|
|
7679
7780
|
category: _enum(["message", "request", "alert"]),
|
|
7680
7781
|
conversation: ConversationRef.optional(),
|
|
7681
7782
|
from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }).optional(),
|
|
7682
7783
|
line: string2().max(NOTICE_LINE_CHARS).optional(),
|
|
7784
|
+
device: NoticeDevice.optional(),
|
|
7683
7785
|
at: Millis
|
|
7684
7786
|
});
|
|
7685
7787
|
NoticeCounts = object({ unread: number2().int().nonnegative(), requests: number2().int().nonnegative() });
|
|
@@ -7724,8 +7826,32 @@ var init_jelly2 = __esm(() => {
|
|
|
7724
7826
|
result: object({ notices: array(Notice), counts: NoticeCounts })
|
|
7725
7827
|
},
|
|
7726
7828
|
"notice.ack": { params: strictObject({ ids: array(NoticeId).min(1).max(200) }), result: object({ ok: literal(true) }) },
|
|
7829
|
+
"daemon.woke": { params: strictObject({}), result: object({ ok: literal(true) }) },
|
|
7830
|
+
"person.attached": { params: strictObject({ attached: boolean2() }), result: object({ ok: literal(true) }) },
|
|
7831
|
+
"wake.arm": {
|
|
7832
|
+
params: strictObject({
|
|
7833
|
+
url: WakeUrl,
|
|
7834
|
+
ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
|
|
7835
|
+
agents: array(SessionName).max(WAKE_AGENTS)
|
|
7836
|
+
}),
|
|
7837
|
+
result: object({ relay: _enum(["armed", "pending"]), awaiting: number2().int().nonnegative() })
|
|
7838
|
+
},
|
|
7839
|
+
"wake.disarm": { params: strictObject({}), result: object({ relay: _enum(["disarmed", "pending"]) }) },
|
|
7840
|
+
"daemon.snapshot": {
|
|
7841
|
+
params: strictObject({ done: literal(true).optional() }),
|
|
7842
|
+
result: union([
|
|
7843
|
+
object({
|
|
7844
|
+
folder: string2().max(4096),
|
|
7845
|
+
files: array(object({ name: string2().max(64), bytes: number2().int().nonnegative(), sha256: string2().regex(/^[0-9a-f]{64}$/) })).max(8),
|
|
7846
|
+
storeSchema: number2().int().positive(),
|
|
7847
|
+
at: Millis,
|
|
7848
|
+
until: Millis
|
|
7849
|
+
}),
|
|
7850
|
+
object({ released: boolean2() })
|
|
7851
|
+
])
|
|
7852
|
+
},
|
|
7727
7853
|
"device.remove.preview": {
|
|
7728
|
-
params:
|
|
7854
|
+
params: DeviceRemoveParams,
|
|
7729
7855
|
result: object({
|
|
7730
7856
|
label: MachineLabel,
|
|
7731
7857
|
kind: DeviceKind.optional(),
|
|
@@ -7739,7 +7865,7 @@ var init_jelly2 = __esm(() => {
|
|
|
7739
7865
|
});
|
|
7740
7866
|
|
|
7741
7867
|
// packages/core/dist/schema/ipc.js
|
|
7742
|
-
var MESSAGING_ERROR_RPC_CODE = -32000, SessionHost, IPC_PROTOCOL = 1, IpcHello, Ok, Empty, DaemonStatus, InviteSummary, IPC_METHODS, AGENT_METHODS, HOST_SCOPE_METHODS, IpcEvent;
|
|
7868
|
+
var MESSAGING_ERROR_RPC_CODE = -32000, SessionHost, IPC_PROTOCOL = 1, IpcHello, Ok, Empty, DaemonStatus, InviteSummary, IPC_METHODS, AGENT_METHODS, HUMAN_METHODS, ADMIN_METHODS, HOST_SCOPE_METHODS, IpcEvent;
|
|
7743
7869
|
var init_ipc = __esm(() => {
|
|
7744
7870
|
init_zod();
|
|
7745
7871
|
init_device();
|
|
@@ -7980,7 +8106,68 @@ var init_ipc = __esm(() => {
|
|
|
7980
8106
|
"task.update",
|
|
7981
8107
|
"task.cancel"
|
|
7982
8108
|
];
|
|
7983
|
-
|
|
8109
|
+
HUMAN_METHODS = [
|
|
8110
|
+
"conv.list",
|
|
8111
|
+
"conv.read",
|
|
8112
|
+
"conv.markRead",
|
|
8113
|
+
"conv.subscribe",
|
|
8114
|
+
"person.send",
|
|
8115
|
+
"people.search",
|
|
8116
|
+
"people.get",
|
|
8117
|
+
"grant.list",
|
|
8118
|
+
"grant.set",
|
|
8119
|
+
"grant.set.preview",
|
|
8120
|
+
"request.list",
|
|
8121
|
+
"request.answer",
|
|
8122
|
+
"request.answer.preview",
|
|
8123
|
+
"contact.request",
|
|
8124
|
+
"contact.request.preview",
|
|
8125
|
+
"card.get",
|
|
8126
|
+
"agents.list",
|
|
8127
|
+
"agents.read",
|
|
8128
|
+
"notice.subscribe",
|
|
8129
|
+
"notice.list",
|
|
8130
|
+
"notice.ack",
|
|
8131
|
+
"device.approve.preview",
|
|
8132
|
+
"device.remove.preview",
|
|
8133
|
+
"identity.replace.preview",
|
|
8134
|
+
"directory.erase.preview",
|
|
8135
|
+
"channel.create",
|
|
8136
|
+
"channel.invite",
|
|
8137
|
+
"channel.leave",
|
|
8138
|
+
"channel.remove"
|
|
8139
|
+
];
|
|
8140
|
+
ADMIN_METHODS = [
|
|
8141
|
+
"daemon.stop",
|
|
8142
|
+
"daemon.woke",
|
|
8143
|
+
"person.attached",
|
|
8144
|
+
"daemon.snapshot",
|
|
8145
|
+
"wake.arm",
|
|
8146
|
+
"wake.disarm",
|
|
8147
|
+
"device.list",
|
|
8148
|
+
"device.remove",
|
|
8149
|
+
"device.link",
|
|
8150
|
+
"device.login",
|
|
8151
|
+
"device.login.status",
|
|
8152
|
+
"device.login.cancel",
|
|
8153
|
+
"device.approve",
|
|
8154
|
+
"identity.rotateEnc",
|
|
8155
|
+
"identity.replace",
|
|
8156
|
+
"directory.erase",
|
|
8157
|
+
"team.create",
|
|
8158
|
+
"team.invite",
|
|
8159
|
+
"team.join",
|
|
8160
|
+
"team.members",
|
|
8161
|
+
"team.remove",
|
|
8162
|
+
"team.role",
|
|
8163
|
+
"team.revokeInvite",
|
|
8164
|
+
"team.add",
|
|
8165
|
+
"team.pair",
|
|
8166
|
+
"team.claim",
|
|
8167
|
+
"team.pairCancel",
|
|
8168
|
+
"team.prepare"
|
|
8169
|
+
];
|
|
8170
|
+
HOST_SCOPE_METHODS = ["notice.subscribe", "notice.list", "conv.list", "daemon.stop", "daemon.woke", "person.attached", "daemon.snapshot", "wake.arm", "wake.disarm"];
|
|
7984
8171
|
IpcEvent = discriminatedUnion("type", [
|
|
7985
8172
|
object({ type: literal("inbox"), key: SessionKey, item: InboxItem }),
|
|
7986
8173
|
object({ type: literal("receipt"), key: SessionKey, envelopeId: Ulid, to: SessionAddress, stage: DeliveryStage, code: ErrorCode.optional() }),
|
|
@@ -8047,7 +8234,7 @@ var init_b64u = __esm(() => {
|
|
|
8047
8234
|
});
|
|
8048
8235
|
|
|
8049
8236
|
// packages/core/dist/version.js
|
|
8050
|
-
var VERSION = "0.
|
|
8237
|
+
var VERSION = "0.6.0";
|
|
8051
8238
|
|
|
8052
8239
|
// packages/core/dist/jcs.js
|
|
8053
8240
|
function hasLoneSurrogate(text) {
|
|
@@ -9273,6 +9460,9 @@ var init_roster2 = __esm(() => {
|
|
|
9273
9460
|
|
|
9274
9461
|
// packages/core/dist/jelly-host.js
|
|
9275
9462
|
import { verify as ed25519Verify2 } from "crypto";
|
|
9463
|
+
function actionClass(method) {
|
|
9464
|
+
return Object.hasOwn(ACTION_CLASSES, method) ? ACTION_CLASSES[method] : "local";
|
|
9465
|
+
}
|
|
9276
9466
|
function grantInput(params) {
|
|
9277
9467
|
return sha256(canonicalize(params ?? {}));
|
|
9278
9468
|
}
|
|
@@ -9342,9 +9532,16 @@ function verifyActGrant(token, check) {
|
|
|
9342
9532
|
throw fail("is for other params");
|
|
9343
9533
|
checkTimes(claims.iat, claims.exp, ACT_GRANT_MAX_S, check.now, fail);
|
|
9344
9534
|
}
|
|
9345
|
-
if (claims.
|
|
9346
|
-
|
|
9347
|
-
|
|
9535
|
+
if (claims.act === "host") {
|
|
9536
|
+
if (claims.cls !== undefined)
|
|
9537
|
+
throw fail("is host scope, which names no action class");
|
|
9538
|
+
} else {
|
|
9539
|
+
if (claims.cls === undefined)
|
|
9540
|
+
throw fail("names no action class");
|
|
9541
|
+
if (claims.cls !== actionClass(check.method))
|
|
9542
|
+
throw fail(`names action class ${claims.cls}, not ${actionClass(check.method)}`);
|
|
9543
|
+
}
|
|
9544
|
+
return { scope: claims.act === "host" ? "host" : "action", jti: claims.jti, exp: claims.exp, dev: claims.dev, mbr: claims.mbr };
|
|
9348
9545
|
}
|
|
9349
9546
|
function verifySessionTicket(token, key, now) {
|
|
9350
9547
|
const fail = (why) => new MessagingError("ticket_invalid", `the session ticket ${why}`);
|
|
@@ -9379,7 +9576,7 @@ function parseActKey(text) {
|
|
|
9379
9576
|
}
|
|
9380
9577
|
return parsed.data;
|
|
9381
9578
|
}
|
|
9382
|
-
var ACT_GRANT_MAX_S = 60, HOST_GRANT_MAX_S = 600, SESSION_TICKET_MAX_S, JELLY_HOST_SKEW_S = 5,
|
|
9579
|
+
var ACT_GRANT_MAX_S = 60, HOST_GRANT_MAX_S = 600, SESSION_TICKET_MAX_S, JELLY_HOST_SKEW_S = 5, CHAT_METHODS, VIEW_METHODS, classOf2 = (method) => CHAT_METHODS.includes(method) ? "chat" : VIEW_METHODS.includes(method) ? "view" : "local", ACTION_CLASSES;
|
|
9383
9580
|
var init_jelly_host = __esm(() => {
|
|
9384
9581
|
init_b64u();
|
|
9385
9582
|
init_hash();
|
|
@@ -9388,8 +9585,24 @@ var init_jelly_host = __esm(() => {
|
|
|
9388
9585
|
init_wire();
|
|
9389
9586
|
init_errors3();
|
|
9390
9587
|
init_jelly();
|
|
9588
|
+
init_ipc();
|
|
9391
9589
|
SESSION_TICKET_MAX_S = 7 * 24 * 60 * 60;
|
|
9392
|
-
|
|
9590
|
+
CHAT_METHODS = ["person.send", "conv.markRead", "notice.ack"];
|
|
9591
|
+
VIEW_METHODS = [
|
|
9592
|
+
"conv.list",
|
|
9593
|
+
"conv.read",
|
|
9594
|
+
"conv.subscribe",
|
|
9595
|
+
"people.search",
|
|
9596
|
+
"people.get",
|
|
9597
|
+
"grant.list",
|
|
9598
|
+
"request.list",
|
|
9599
|
+
"agents.list",
|
|
9600
|
+
"agents.read",
|
|
9601
|
+
"notice.subscribe",
|
|
9602
|
+
"notice.list",
|
|
9603
|
+
"device.list"
|
|
9604
|
+
];
|
|
9605
|
+
ACTION_CLASSES = Object.freeze(Object.fromEntries([...HUMAN_METHODS, ...ADMIN_METHODS].map((method) => [method, classOf2(method)])));
|
|
9393
9606
|
});
|
|
9394
9607
|
|
|
9395
9608
|
// packages/core/dist/directory/person.js
|
|
@@ -9830,6 +10043,13 @@ class DeviceLog {
|
|
|
9830
10043
|
token(seq) {
|
|
9831
10044
|
return this.#state.tokens.get(seq);
|
|
9832
10045
|
}
|
|
10046
|
+
spentSignIn(jti) {
|
|
10047
|
+
for (const [seq, token] of this.#state.tokens) {
|
|
10048
|
+
if (token.claims.src === JELLY_DEVICE_SOURCE && token.claims.jti === jti && this.#state.entries[seq].op !== "unbind")
|
|
10049
|
+
return true;
|
|
10050
|
+
}
|
|
10051
|
+
return false;
|
|
10052
|
+
}
|
|
9833
10053
|
bindingDeadline(seq) {
|
|
9834
10054
|
const token = this.#state.tokens.get(seq);
|
|
9835
10055
|
return token === undefined ? undefined : token.claims.exp * 1000 + LIMITS.clockSkewMs;
|
|
@@ -9882,6 +10102,14 @@ class DeviceLog {
|
|
|
9882
10102
|
const { claims } = token;
|
|
9883
10103
|
if (personId(claims.iss, claims.sub) !== this.person)
|
|
9884
10104
|
throw refuse("Jelly token names another person");
|
|
10105
|
+
if (claims.src === JELLY_DEVICE_SOURCE) {
|
|
10106
|
+
for (const [seq, used] of state.tokens) {
|
|
10107
|
+
if (used.claims.src !== JELLY_DEVICE_SOURCE || used.claims.jti !== claims.jti)
|
|
10108
|
+
continue;
|
|
10109
|
+
if (parsed.op !== "unbind" || state.entries[seq].op !== "unbind")
|
|
10110
|
+
throw refuse("a sign-in's token vouches for one act: this one was used");
|
|
10111
|
+
}
|
|
10112
|
+
}
|
|
9885
10113
|
if (parsed.ts < claims.iat * 1000 - LIMITS.clockSkewMs || parsed.ts > claims.exp * 1000 + LIMITS.clockSkewMs)
|
|
9886
10114
|
throw refuse("log entry is dated outside its Jelly token's life");
|
|
9887
10115
|
tokens.set(parsed.seq, token);
|
|
@@ -9966,6 +10194,7 @@ var init_device_log = __esm(() => {
|
|
|
9966
10194
|
init_keys();
|
|
9967
10195
|
init_sign();
|
|
9968
10196
|
init_common();
|
|
10197
|
+
init_jelly();
|
|
9969
10198
|
init_directory();
|
|
9970
10199
|
init_errors3();
|
|
9971
10200
|
init_jelly_token();
|
|
@@ -12634,6 +12863,7 @@ var init_dist = __esm(() => {
|
|
|
12634
12863
|
init_contact_card();
|
|
12635
12864
|
init_delegation2();
|
|
12636
12865
|
init_task();
|
|
12866
|
+
init_a2a();
|
|
12637
12867
|
init_direct2();
|
|
12638
12868
|
init_peer_text();
|
|
12639
12869
|
init_agent_tools();
|
|
@@ -15583,7 +15813,8 @@ function presencePayload(rows, label, seq, extra, policy) {
|
|
|
15583
15813
|
label,
|
|
15584
15814
|
sessions: [],
|
|
15585
15815
|
features: [...FEATURES, ...extra?.features ?? []],
|
|
15586
|
-
...extra?.direct ? { direct: extra.direct } : {}
|
|
15816
|
+
...extra?.direct ? { direct: extra.direct } : {},
|
|
15817
|
+
...policy.away === true ? { away: true } : {}
|
|
15587
15818
|
};
|
|
15588
15819
|
return { ...payload, sessions: fitPresence(sessions, jcsBytes(payload), LIMITS.presenceBytes) };
|
|
15589
15820
|
}
|
|
@@ -15810,14 +16041,16 @@ class RosterIdentity {
|
|
|
15810
16041
|
#machine;
|
|
15811
16042
|
#catchUp;
|
|
15812
16043
|
#now;
|
|
15813
|
-
#listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
|
|
16044
|
+
#listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
|
|
15814
16045
|
#me;
|
|
16046
|
+
#length;
|
|
15815
16047
|
constructor(options) {
|
|
15816
16048
|
this.roster = options.roster;
|
|
15817
16049
|
this.#machine = options.machine;
|
|
15818
16050
|
this.#catchUp = options.catchUp;
|
|
15819
16051
|
this.#now = options.now;
|
|
15820
16052
|
this.#me = this.#selfRecord();
|
|
16053
|
+
this.#length = options.roster.entries().length;
|
|
15821
16054
|
}
|
|
15822
16055
|
get realm() {
|
|
15823
16056
|
return this.roster.realm;
|
|
@@ -15848,6 +16081,31 @@ class RosterIdentity {
|
|
|
15848
16081
|
successor(_device) {
|
|
15849
16082
|
return;
|
|
15850
16083
|
}
|
|
16084
|
+
deviceChanges(person) {
|
|
16085
|
+
const labels = new Map;
|
|
16086
|
+
const out = [];
|
|
16087
|
+
for (const entry of this.roster.entries()) {
|
|
16088
|
+
const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
|
|
16089
|
+
if (member) {
|
|
16090
|
+
labels.set(member.machine, member.label);
|
|
16091
|
+
if (member.person === person && entry.kind !== "genesis")
|
|
16092
|
+
out.push({ seq: entry.seq, change: "added", device: member.machine, label: member.label, vouch: "admin", by: entry.by, at: entry.ts });
|
|
16093
|
+
} else if (entry.kind === "member.remove") {
|
|
16094
|
+
const owner = this.#ownerAt(entry.body.machine);
|
|
16095
|
+
if (owner === person)
|
|
16096
|
+
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 });
|
|
16097
|
+
}
|
|
16098
|
+
}
|
|
16099
|
+
return out;
|
|
16100
|
+
}
|
|
16101
|
+
#ownerAt(machine) {
|
|
16102
|
+
for (const entry of this.roster.entries()) {
|
|
16103
|
+
const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
|
|
16104
|
+
if (member?.machine === machine)
|
|
16105
|
+
return member.person;
|
|
16106
|
+
}
|
|
16107
|
+
return;
|
|
16108
|
+
}
|
|
15851
16109
|
async devices(person, opts = {}) {
|
|
15852
16110
|
if (opts.atLeast !== undefined && this.relate(opts.atLeast).relation === "ahead") {
|
|
15853
16111
|
throw new MessagingError("roster_conflict", "the roster has not reached that anchor");
|
|
@@ -15891,6 +16149,20 @@ class RosterIdentity {
|
|
|
15891
16149
|
}
|
|
15892
16150
|
for (const listener of [...this.#listeners["devices-changed"]])
|
|
15893
16151
|
listener();
|
|
16152
|
+
const entries = this.roster.entries();
|
|
16153
|
+
if (entries.length !== this.#length) {
|
|
16154
|
+
const persons = new Set;
|
|
16155
|
+
for (const entry of entries.slice(this.#length)) {
|
|
16156
|
+
const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : undefined;
|
|
16157
|
+
const person = member?.person ?? (entry.kind === "member.remove" ? this.#ownerAt(entry.body.machine) : undefined);
|
|
16158
|
+
if (person !== undefined)
|
|
16159
|
+
persons.add(person);
|
|
16160
|
+
}
|
|
16161
|
+
this.#length = entries.length;
|
|
16162
|
+
if (persons.size > 0)
|
|
16163
|
+
for (const listener of [...this.#listeners["device-log"]])
|
|
16164
|
+
listener([...persons].sort());
|
|
16165
|
+
}
|
|
15894
16166
|
}
|
|
15895
16167
|
#selfRecord() {
|
|
15896
16168
|
const me = this.roster.member(this.#machine);
|
|
@@ -16106,16 +16378,18 @@ class DirectoryIdentity {
|
|
|
16106
16378
|
#options;
|
|
16107
16379
|
#cache;
|
|
16108
16380
|
#keyring;
|
|
16109
|
-
#listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
|
|
16381
|
+
#listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
|
|
16110
16382
|
#head;
|
|
16111
16383
|
#persons = new Map;
|
|
16112
16384
|
#index = new Map;
|
|
16113
16385
|
#own;
|
|
16114
16386
|
#relations = new Map;
|
|
16115
16387
|
#watching = new Set;
|
|
16388
|
+
#watchRead = Promise.resolve();
|
|
16116
16389
|
#alarm;
|
|
16117
16390
|
#selfRecord;
|
|
16118
16391
|
#monitoredAt = Number.NEGATIVE_INFINITY;
|
|
16392
|
+
#bindAccepted = false;
|
|
16119
16393
|
#pendingLink;
|
|
16120
16394
|
constructor(options) {
|
|
16121
16395
|
this.#options = options;
|
|
@@ -16175,7 +16449,10 @@ class DirectoryIdentity {
|
|
|
16175
16449
|
const person = this.#own.person;
|
|
16176
16450
|
if (!person)
|
|
16177
16451
|
return false;
|
|
16178
|
-
const
|
|
16452
|
+
const held = this.#persons.get(person)?.state;
|
|
16453
|
+
if (held?.erased)
|
|
16454
|
+
return true;
|
|
16455
|
+
const record = held?.log.device(this.#machine());
|
|
16179
16456
|
return record !== undefined && !record.current;
|
|
16180
16457
|
}
|
|
16181
16458
|
persons() {
|
|
@@ -16196,6 +16473,12 @@ class DirectoryIdentity {
|
|
|
16196
16473
|
}
|
|
16197
16474
|
return;
|
|
16198
16475
|
}
|
|
16476
|
+
deviceChanges(person) {
|
|
16477
|
+
const held = isHosted(person) ? this.#persons.get(person) : undefined;
|
|
16478
|
+
if (!held || held.state.erased)
|
|
16479
|
+
return [];
|
|
16480
|
+
return deviceChangesOf(held.state.log);
|
|
16481
|
+
}
|
|
16199
16482
|
async devices(person, opts = {}) {
|
|
16200
16483
|
if (!isHosted(person))
|
|
16201
16484
|
return { person, devices: [], anchor: this.anchor(), fetchedAt: this.#options.now() };
|
|
@@ -16290,15 +16573,23 @@ class DirectoryIdentity {
|
|
|
16290
16573
|
}
|
|
16291
16574
|
}
|
|
16292
16575
|
watch(persons) {
|
|
16293
|
-
|
|
16576
|
+
const added = [];
|
|
16294
16577
|
for (const person of persons) {
|
|
16295
16578
|
if (!isHosted(person) || this.#watching.has(person) || this.#watching.size >= LIMITS.watchPersons)
|
|
16296
16579
|
continue;
|
|
16297
16580
|
this.#watching.add(person);
|
|
16298
|
-
|
|
16581
|
+
added.push(person);
|
|
16299
16582
|
}
|
|
16300
|
-
if (
|
|
16301
|
-
|
|
16583
|
+
if (added.length === 0)
|
|
16584
|
+
return this.#watchRead;
|
|
16585
|
+
this.#watchRead = this.#call((link) => link.watch([...this.#watching])).then(async () => {
|
|
16586
|
+
const held = added.filter((person) => this.#persons.has(person));
|
|
16587
|
+
if (held.length === 0)
|
|
16588
|
+
return;
|
|
16589
|
+
await this.#latest();
|
|
16590
|
+
await this.#fetch(held);
|
|
16591
|
+
}).catch(() => {});
|
|
16592
|
+
return this.#watchRead;
|
|
16302
16593
|
}
|
|
16303
16594
|
on(event, listener) {
|
|
16304
16595
|
const listeners = this.#listeners[event];
|
|
@@ -16309,6 +16600,10 @@ class DirectoryIdentity {
|
|
|
16309
16600
|
listeners.splice(at, 1);
|
|
16310
16601
|
};
|
|
16311
16602
|
}
|
|
16603
|
+
linkPerson() {
|
|
16604
|
+
const binding = this.binding();
|
|
16605
|
+
return binding.state !== "none" || this.#bindAccepted ? binding.person : undefined;
|
|
16606
|
+
}
|
|
16312
16607
|
alarm() {
|
|
16313
16608
|
return this.#alarm;
|
|
16314
16609
|
}
|
|
@@ -16318,6 +16613,8 @@ class DirectoryIdentity {
|
|
|
16318
16613
|
binding() {
|
|
16319
16614
|
const person = this.#own.person;
|
|
16320
16615
|
const record = person ? this.#persons.get(person)?.state.log.device(this.#machine()) : undefined;
|
|
16616
|
+
if (person && this.#persons.get(person)?.state.erased)
|
|
16617
|
+
return { state: "removed", person };
|
|
16321
16618
|
if (!person || !record)
|
|
16322
16619
|
return { state: "none", ...person ? { person } : {} };
|
|
16323
16620
|
if (!record.current)
|
|
@@ -16358,13 +16655,29 @@ class DirectoryIdentity {
|
|
|
16358
16655
|
authority: { kind: "jelly", token }
|
|
16359
16656
|
});
|
|
16360
16657
|
this.#signed(entry, person);
|
|
16361
|
-
|
|
16658
|
+
if (current)
|
|
16659
|
+
return await this.#call((link) => link.append(entry));
|
|
16660
|
+
const bound = await this.#call((link) => link.bind(entry));
|
|
16661
|
+
this.#bindAccepted = true;
|
|
16662
|
+
return bound;
|
|
16362
16663
|
});
|
|
16363
16664
|
await this.#adoptAnnounced(done.sth);
|
|
16364
16665
|
await this.#fetch([person]);
|
|
16365
16666
|
await this.publishKeys();
|
|
16366
16667
|
return { person, seq: done.seq };
|
|
16367
16668
|
}
|
|
16669
|
+
async awaitSelf() {
|
|
16670
|
+
const person = this.#own.person;
|
|
16671
|
+
if (!person)
|
|
16672
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
16673
|
+
for (let tries = 0;this.self() === undefined || !this.#index.has(this.#machine()); tries++) {
|
|
16674
|
+
if (tries >= LOG_RETRIES)
|
|
16675
|
+
throw unavailable("the directory has not committed this device's binding and keys yet");
|
|
16676
|
+
await new Promise((resolve) => setTimeout(resolve, LOG_RETRY_MS));
|
|
16677
|
+
await this.#latest();
|
|
16678
|
+
await this.#fetch([person]);
|
|
16679
|
+
}
|
|
16680
|
+
}
|
|
16368
16681
|
async publishKeys() {
|
|
16369
16682
|
if (this.#alarm?.kind === "restored")
|
|
16370
16683
|
throw new MessagingError("invalid_state", "this device runs from an older copy of its home; it publishes no keys");
|
|
@@ -16415,6 +16728,45 @@ class DirectoryIdentity {
|
|
|
16415
16728
|
await this.#fetch([person]);
|
|
16416
16729
|
return { seq: done.seq };
|
|
16417
16730
|
}
|
|
16731
|
+
async erase(token) {
|
|
16732
|
+
const identity = this.#options.identity();
|
|
16733
|
+
const person = this.#own.person;
|
|
16734
|
+
if (!person || this.binding().state === "none" || this.removed())
|
|
16735
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
16736
|
+
if (this.binding().record?.vouch !== "jelly")
|
|
16737
|
+
throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
|
|
16738
|
+
const entries = await this.#extendLog(async () => {
|
|
16739
|
+
await this.#latest();
|
|
16740
|
+
await this.#fetch([person]);
|
|
16741
|
+
const log = this.#persons.get(person).state.log;
|
|
16742
|
+
let head = log.head();
|
|
16743
|
+
const out = [];
|
|
16744
|
+
const devices = log.devices().sort((a, b) => Number(a.device === identity.machine) - Number(b.device === identity.machine));
|
|
16745
|
+
for (const device of devices) {
|
|
16746
|
+
const self = device.device === identity.machine;
|
|
16747
|
+
const entry = signLogEntry(identity, {
|
|
16748
|
+
v: PROTOCOL,
|
|
16749
|
+
realm: this.realm,
|
|
16750
|
+
person,
|
|
16751
|
+
seq: head.seq + 1,
|
|
16752
|
+
prev: head.hash,
|
|
16753
|
+
ts: this.#options.now(),
|
|
16754
|
+
op: "unbind",
|
|
16755
|
+
device: device.device,
|
|
16756
|
+
reason: self ? "self" : "removed",
|
|
16757
|
+
authority: self ? { kind: "self" } : { kind: "jelly", token }
|
|
16758
|
+
});
|
|
16759
|
+
out.push(entry);
|
|
16760
|
+
head = { seq: entry.seq, hash: logEntryHash(entry) };
|
|
16761
|
+
}
|
|
16762
|
+
for (const entry of out)
|
|
16763
|
+
this.#signed(entry, person);
|
|
16764
|
+
await this.#call((link) => link.erase(out, token));
|
|
16765
|
+
return out;
|
|
16766
|
+
});
|
|
16767
|
+
await this.#latest().then(() => this.#fetch([person])).catch(() => {});
|
|
16768
|
+
return { person, devices: entries.length, seq: entries.at(-1).seq };
|
|
16769
|
+
}
|
|
16418
16770
|
createLink(args) {
|
|
16419
16771
|
if (this.#own.person !== undefined) {
|
|
16420
16772
|
throw new MessagingError("invalid_state", this.removed() ? "this device was removed from its account; it cannot be bound again" : "this device is already bound");
|
|
@@ -16633,15 +16985,25 @@ class DirectoryIdentity {
|
|
|
16633
16985
|
throw new MessagingError("directory_inconsistent", "the directory answered at another tree head");
|
|
16634
16986
|
const now = this.#options.now();
|
|
16635
16987
|
const views = new Map(answer.persons.map((view) => [view.person, view]));
|
|
16988
|
+
const grew = [];
|
|
16636
16989
|
for (const person of persons) {
|
|
16637
16990
|
const view = views.get(person);
|
|
16638
16991
|
if (!view)
|
|
16639
16992
|
continue;
|
|
16993
|
+
const before = this.#persons.get(person)?.state;
|
|
16640
16994
|
const state = await this.#verify(view, head);
|
|
16641
16995
|
this.#persons.set(person, { state, size: head.sth.size, fetchedAt: now });
|
|
16642
16996
|
this.#cache.putPerson({ person, entries: [...state.log.entries()], keys: [...state.keys.values()], erased: state.erased, size: head.sth.size, fetchedAt: now });
|
|
16997
|
+
if (state.log.length !== (before?.log.length ?? 0) || state.erased !== (before?.erased ?? false))
|
|
16998
|
+
grew.push(person);
|
|
16643
16999
|
}
|
|
16644
17000
|
this.#changed();
|
|
17001
|
+
if (grew.length > 0)
|
|
17002
|
+
for (const listener of [...this.#listeners["device-log"]])
|
|
17003
|
+
listener(grew);
|
|
17004
|
+
const unwatched = persons.filter((person) => views.has(person) && !this.#watching.has(person));
|
|
17005
|
+
if (unwatched.length > 0)
|
|
17006
|
+
this.watch(unwatched);
|
|
16645
17007
|
}
|
|
16646
17008
|
async#verify(view, head) {
|
|
16647
17009
|
const known = this.#persons.get(view.person)?.state;
|
|
@@ -16763,6 +17125,56 @@ ${this.#index.get(me.machine)?.enc ?? ""}`;
|
|
|
16763
17125
|
return mine === theirs ? "same" : "other";
|
|
16764
17126
|
}
|
|
16765
17127
|
}
|
|
17128
|
+
function deviceChangesOf(log) {
|
|
17129
|
+
const vouch = new Map;
|
|
17130
|
+
const record = new Map;
|
|
17131
|
+
const out = [];
|
|
17132
|
+
for (const entry of log.entries()) {
|
|
17133
|
+
const token = log.token(entry.seq);
|
|
17134
|
+
const via = token === undefined ? undefined : token.claims.hst !== undefined ? "host" : "browser";
|
|
17135
|
+
if (entry.op === "bind") {
|
|
17136
|
+
const v = entry.authority.kind === "jelly" ? "jelly" : "device";
|
|
17137
|
+
vouch.set(entry.device, v);
|
|
17138
|
+
record.set(entry.device, { label: entry.label, kind: entry.kind, host: via === "host" });
|
|
17139
|
+
out.push({
|
|
17140
|
+
seq: entry.seq,
|
|
17141
|
+
change: "added",
|
|
17142
|
+
device: entry.device,
|
|
17143
|
+
label: entry.label,
|
|
17144
|
+
kind: entry.kind,
|
|
17145
|
+
vouch: v,
|
|
17146
|
+
...via ? { via } : {},
|
|
17147
|
+
...entry.authority.kind === "device" ? { approver: entry.authority.approver } : {},
|
|
17148
|
+
by: entry.by,
|
|
17149
|
+
at: entry.ts
|
|
17150
|
+
});
|
|
17151
|
+
} else if (entry.op === "attest") {
|
|
17152
|
+
const known = record.get(entry.device);
|
|
17153
|
+
const next = { label: entry.label ?? known?.label ?? "device", kind: entry.kind ?? known?.kind ?? "other", host: via === "host" };
|
|
17154
|
+
record.set(entry.device, next);
|
|
17155
|
+
const raised = vouch.get(entry.device) !== "jelly";
|
|
17156
|
+
vouch.set(entry.device, "jelly");
|
|
17157
|
+
if (raised)
|
|
17158
|
+
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 });
|
|
17159
|
+
} else {
|
|
17160
|
+
const known = record.get(entry.device);
|
|
17161
|
+
const v = vouch.get(entry.device) ?? "device";
|
|
17162
|
+
out.push({
|
|
17163
|
+
seq: entry.seq,
|
|
17164
|
+
change: entry.reason === "replaced" ? "replaced" : "removed",
|
|
17165
|
+
device: entry.device,
|
|
17166
|
+
label: known?.label ?? "device",
|
|
17167
|
+
...known ? { kind: known.kind } : {},
|
|
17168
|
+
vouch: v,
|
|
17169
|
+
...v === "jelly" && known ? { via: known.host ? "host" : "browser" } : {},
|
|
17170
|
+
by: entry.by,
|
|
17171
|
+
reason: entry.reason,
|
|
17172
|
+
at: entry.ts
|
|
17173
|
+
});
|
|
17174
|
+
}
|
|
17175
|
+
}
|
|
17176
|
+
return out;
|
|
17177
|
+
}
|
|
16766
17178
|
function orgOf(email, domains) {
|
|
16767
17179
|
if (email === undefined)
|
|
16768
17180
|
return;
|
|
@@ -16847,7 +17259,7 @@ class DirectoryHome {
|
|
|
16847
17259
|
url: this.#options.config.url,
|
|
16848
17260
|
realm: realm.realm,
|
|
16849
17261
|
identity: this.#options.identity,
|
|
16850
|
-
person: () => realm.
|
|
17262
|
+
person: () => realm.linkPerson(),
|
|
16851
17263
|
pushes: {
|
|
16852
17264
|
changed: (persons, sth) => void realm.changed(persons, sth).catch((error) => this.#failed("directory.changed", error)),
|
|
16853
17265
|
unbound: (entry, sth) => void realm.changed([entry.person], sth).catch((error) => this.#failed("directory.unbound", error)),
|
|
@@ -16999,7 +17411,27 @@ class DirectoryHome {
|
|
|
16999
17411
|
this.#login.cancel();
|
|
17000
17412
|
return { ok: true };
|
|
17001
17413
|
}
|
|
17414
|
+
async signIn(purpose, args) {
|
|
17415
|
+
this.#removing = undefined;
|
|
17416
|
+
this.#binding = undefined;
|
|
17417
|
+
const started = await this.#login.start({ purpose, label: args.label, kind: args.kind });
|
|
17418
|
+
return {
|
|
17419
|
+
purpose,
|
|
17420
|
+
verificationUriComplete: started.verificationUriComplete,
|
|
17421
|
+
verificationUri: started.verificationUri,
|
|
17422
|
+
userCode: started.userCode,
|
|
17423
|
+
fingerprint: started.fingerprint,
|
|
17424
|
+
expires: started.expires,
|
|
17425
|
+
interval: started.interval
|
|
17426
|
+
};
|
|
17427
|
+
}
|
|
17002
17428
|
async#completeLogin(token, purpose) {
|
|
17429
|
+
if (purpose === "replace" || purpose === "erase") {
|
|
17430
|
+
const step = purpose === "replace" ? this.#options.replaceStep : this.#options.eraseStep;
|
|
17431
|
+
if (!step)
|
|
17432
|
+
throw new MessagingError("invalid_state", `this daemon does not ${purpose} through a sign-in`);
|
|
17433
|
+
return step(token);
|
|
17434
|
+
}
|
|
17003
17435
|
if (purpose === "remove") {
|
|
17004
17436
|
const removing = this.#removing;
|
|
17005
17437
|
if (!removing)
|
|
@@ -17019,6 +17451,12 @@ class DirectoryHome {
|
|
|
17019
17451
|
this.#options.log("info", "directory.approved", { seq: done.seq });
|
|
17020
17452
|
return done;
|
|
17021
17453
|
}
|
|
17454
|
+
async wake(state) {
|
|
17455
|
+
const link = this.#link;
|
|
17456
|
+
if (!link?.wake)
|
|
17457
|
+
return "pending";
|
|
17458
|
+
return link.wake(state);
|
|
17459
|
+
}
|
|
17022
17460
|
presenceWatch(persons) {
|
|
17023
17461
|
this.#link?.presenceWatch(persons).catch((error) => this.#failed("directory.presence.watch", error));
|
|
17024
17462
|
}
|
|
@@ -17033,6 +17471,7 @@ class DirectoryHome {
|
|
|
17033
17471
|
},
|
|
17034
17472
|
send: (env) => home.#link ? home.#link.send(env) : Promise.reject(noLink()),
|
|
17035
17473
|
presence: (env) => home.#link?.presence(env),
|
|
17474
|
+
retryNow: () => home.#link?.retryNow?.(),
|
|
17036
17475
|
appendRoster: () => Promise.reject(new MessagingError("invalid_state", "a directory realm has no roster")),
|
|
17037
17476
|
syncRoster: () => Promise.reject(new MessagingError("invalid_state", "a directory realm has no roster"))
|
|
17038
17477
|
};
|
|
@@ -17058,6 +17497,28 @@ var init_directory_home = __esm(() => {
|
|
|
17058
17497
|
init_directory_identity();
|
|
17059
17498
|
});
|
|
17060
17499
|
|
|
17500
|
+
// packages/daemon/dist/realm/directory-cache.js
|
|
17501
|
+
function memoryDirectoryCache(from) {
|
|
17502
|
+
let head = from?.head();
|
|
17503
|
+
const persons = new Map((from?.persons() ?? []).map((p) => [p.person, p]));
|
|
17504
|
+
let archive = from?.archive() ?? [];
|
|
17505
|
+
let own = from?.own() ?? { keysSeq: -1, signed: [] };
|
|
17506
|
+
let alarm = from?.alarm();
|
|
17507
|
+
const copy = (value) => structuredClone(value);
|
|
17508
|
+
return {
|
|
17509
|
+
head: () => copy(head),
|
|
17510
|
+
setHead: (value) => void (head = copy(value)),
|
|
17511
|
+
persons: () => [...persons.values()].map(copy),
|
|
17512
|
+
putPerson: (value) => void persons.set(value.person, copy(value)),
|
|
17513
|
+
archive: () => copy(archive),
|
|
17514
|
+
setArchive: (keys) => void (archive = copy(keys)),
|
|
17515
|
+
own: () => copy(own),
|
|
17516
|
+
setOwn: (value) => void (own = copy(value)),
|
|
17517
|
+
alarm: () => copy(alarm),
|
|
17518
|
+
setAlarm: (value) => void (alarm = copy(value))
|
|
17519
|
+
};
|
|
17520
|
+
}
|
|
17521
|
+
|
|
17061
17522
|
// packages/daemon/dist/jelly.js
|
|
17062
17523
|
import { closeSync as closeSync6, constants as constants6, fstatSync as fstatSync3, readSync } from "fs";
|
|
17063
17524
|
function jellyLabel(text) {
|
|
@@ -17147,7 +17608,12 @@ class JellyAuth {
|
|
|
17147
17608
|
return refuse(new MessagingError("grant_invalid", "the host-scope grant is bound to another connection"));
|
|
17148
17609
|
}
|
|
17149
17610
|
}
|
|
17150
|
-
this.#deps.log("info", "jelly.grant", {
|
|
17611
|
+
this.#deps.log("info", "jelly.grant", {
|
|
17612
|
+
method: call.method,
|
|
17613
|
+
scope: grant.scope,
|
|
17614
|
+
...grant.dev !== undefined ? { dev: grant.dev.slice(0, 200) } : {},
|
|
17615
|
+
...grant.mbr !== undefined ? { mbr: grant.mbr.slice(0, 200) } : {}
|
|
17616
|
+
});
|
|
17151
17617
|
return { scope: grant.scope, expiresAt: (grant.exp + JELLY_HOST_SKEW_S) * 1000 };
|
|
17152
17618
|
}
|
|
17153
17619
|
ticket(p) {
|
|
@@ -17185,6 +17651,292 @@ var init_jelly3 = __esm(() => {
|
|
|
17185
17651
|
HOST_SCOPE = new Set(HOST_SCOPE_METHODS);
|
|
17186
17652
|
});
|
|
17187
17653
|
|
|
17654
|
+
// packages/daemon/dist/jelly-env.js
|
|
17655
|
+
import { delimiter, isAbsolute } from "path";
|
|
17656
|
+
function jellyEnvNamed(env) {
|
|
17657
|
+
return JELLY_ENV.filter((name) => (env[name] ?? "") !== "");
|
|
17658
|
+
}
|
|
17659
|
+
function fail2(message) {
|
|
17660
|
+
throw new MessagingError("invalid_request", message);
|
|
17661
|
+
}
|
|
17662
|
+
function required3(env, name) {
|
|
17663
|
+
const value = env[name] ?? "";
|
|
17664
|
+
if (value === "")
|
|
17665
|
+
fail2(`${name} is not set: a Jelly host sets it with JELLY_ACTION_KEY`);
|
|
17666
|
+
return value;
|
|
17667
|
+
}
|
|
17668
|
+
function folder(name, value, separator = delimiter) {
|
|
17669
|
+
if (!isAbsolute(value))
|
|
17670
|
+
fail2(`${name} is not an absolute path`);
|
|
17671
|
+
if (value.includes(separator))
|
|
17672
|
+
fail2(`${name} names more than one folder (joined with "${separator}"); the daemon takes one`);
|
|
17673
|
+
return value;
|
|
17674
|
+
}
|
|
17675
|
+
function relayUrlOf(url) {
|
|
17676
|
+
let parsed;
|
|
17677
|
+
try {
|
|
17678
|
+
parsed = new URL(url);
|
|
17679
|
+
} catch {
|
|
17680
|
+
return fail2(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not a URL`);
|
|
17681
|
+
}
|
|
17682
|
+
const loopback = /^(?:localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\])$/.test(parsed.hostname);
|
|
17683
|
+
const scheme = parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" && loopback ? "ws:" : undefined;
|
|
17684
|
+
if (scheme === undefined)
|
|
17685
|
+
fail2(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not https:// (or http:// to a loopback host)`);
|
|
17686
|
+
if (parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "" || parsed.pathname !== "/" && parsed.pathname !== "") {
|
|
17687
|
+
fail2(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not an origin: the relay is derived from its origin alone`);
|
|
17688
|
+
}
|
|
17689
|
+
return `${scheme}//${parsed.host}/v1/connect`;
|
|
17690
|
+
}
|
|
17691
|
+
function jellyEnvRealm(env) {
|
|
17692
|
+
let raw;
|
|
17693
|
+
try {
|
|
17694
|
+
raw = parseWire(required3(env, "JELLY_RESOURCES"), { maxBytes: 64 * 1024 });
|
|
17695
|
+
} catch (error) {
|
|
17696
|
+
if (error instanceof MessagingError && /JELLY_RESOURCES is not set/.test(error.message))
|
|
17697
|
+
throw error;
|
|
17698
|
+
return fail2("JELLY_RESOURCES is not JSON");
|
|
17699
|
+
}
|
|
17700
|
+
const list = Resources.safeParse(raw);
|
|
17701
|
+
if (!list.success)
|
|
17702
|
+
fail2("JELLY_RESOURCES is not a list of discovery entries");
|
|
17703
|
+
const entries = list.data.map((item) => ResourceEntry.safeParse(item)).filter((parsed) => parsed.success && parsed.data.name === JELLY_RESOURCE_NAME);
|
|
17704
|
+
if (entries.length === 0)
|
|
17705
|
+
fail2(`JELLY_RESOURCES has no ${JELLY_RESOURCE_NAME} entry`);
|
|
17706
|
+
if (entries.length > 1)
|
|
17707
|
+
fail2(`JELLY_RESOURCES has more than one ${JELLY_RESOURCE_NAME} entry`);
|
|
17708
|
+
const entry = entries[0].data;
|
|
17709
|
+
const realm = DirectoryRealmConfig.strict().safeParse({
|
|
17710
|
+
kind: "directory",
|
|
17711
|
+
url: relayUrlOf(entry.url),
|
|
17712
|
+
issuer: required3(env, "JELLY_ID_ISSUER"),
|
|
17713
|
+
resource: entry.resource,
|
|
17714
|
+
key: required3(env, "JELLY_SERVICE_ANCHOR")
|
|
17715
|
+
});
|
|
17716
|
+
if (!realm.success) {
|
|
17717
|
+
const at = realm.error.issues[0]?.path[0];
|
|
17718
|
+
fail2(at === "issuer" ? "JELLY_ID_ISSUER is not an https:// issuer" : at === "key" ? "JELLY_SERVICE_ANCHOR is not an Ed25519 key in base64url" : `JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} entry is invalid at ${String(at)}`);
|
|
17719
|
+
}
|
|
17720
|
+
try {
|
|
17721
|
+
directoryRealmId(realm.data);
|
|
17722
|
+
} catch {
|
|
17723
|
+
fail2("JELLY_SERVICE_ANCHOR is not a usable Ed25519 key");
|
|
17724
|
+
}
|
|
17725
|
+
return realm.data;
|
|
17726
|
+
}
|
|
17727
|
+
function readJellyEnv(env) {
|
|
17728
|
+
const actKeyPath = required3(env, "JELLY_ACTION_KEY");
|
|
17729
|
+
if (!isAbsolute(actKeyPath))
|
|
17730
|
+
fail2("JELLY_ACTION_KEY is not an absolute path");
|
|
17731
|
+
const home = folder("STATE_DIRECTORY", required3(env, "STATE_DIRECTORY"));
|
|
17732
|
+
const runText = env.RUNTIME_DIRECTORY ?? "";
|
|
17733
|
+
const groupText = env.JELLY_SOCKET_GROUP ?? "";
|
|
17734
|
+
if (groupText !== "" && process.platform === "win32")
|
|
17735
|
+
fail2("JELLY_SOCKET_GROUP is not supported on Windows: a named pipe is not shared with a group");
|
|
17736
|
+
if (groupText !== "" && (!/^\d{1,10}$/.test(groupText) || Number(groupText) > 4294967294))
|
|
17737
|
+
fail2("JELLY_SOCKET_GROUP is not a numeric gid");
|
|
17738
|
+
let label;
|
|
17739
|
+
try {
|
|
17740
|
+
label = jellyLabel(required3(env, "JELLY_DEVICE_LABEL"));
|
|
17741
|
+
} catch (error) {
|
|
17742
|
+
if (error instanceof MessagingError && /is not set/.test(error.message))
|
|
17743
|
+
throw error;
|
|
17744
|
+
return fail2("JELLY_DEVICE_LABEL has no letter or digit to make a device label from");
|
|
17745
|
+
}
|
|
17746
|
+
return {
|
|
17747
|
+
home,
|
|
17748
|
+
...runText !== "" ? { run: folder("RUNTIME_DIRECTORY", runText) } : {},
|
|
17749
|
+
actKeyPath,
|
|
17750
|
+
label,
|
|
17751
|
+
...groupText !== "" ? { socketGroup: Number(groupText) } : {},
|
|
17752
|
+
realm: jellyEnvRealm(env)
|
|
17753
|
+
};
|
|
17754
|
+
}
|
|
17755
|
+
function realmMismatch(pinned, named) {
|
|
17756
|
+
return ["url", "issuer", "resource", "key"].filter((member) => pinned[member] !== named[member]);
|
|
17757
|
+
}
|
|
17758
|
+
async function pinRealm(realm, get = wellKnownGet) {
|
|
17759
|
+
const origin = new URL(realm.url);
|
|
17760
|
+
const url = `${origin.protocol === "wss:" ? "https:" : "http:"}//${origin.host}/.well-known/messaging.json`;
|
|
17761
|
+
let text;
|
|
17762
|
+
try {
|
|
17763
|
+
text = await get(url);
|
|
17764
|
+
} catch (error) {
|
|
17765
|
+
throw new MessagingError("relay_unavailable", `cannot read the relay's realm document ${url} for the first start (${error instanceof Error ? error.message.slice(0, 200) : "failed"}); the next start tries again`);
|
|
17766
|
+
}
|
|
17767
|
+
let raw;
|
|
17768
|
+
try {
|
|
17769
|
+
raw = parseWire(text, { maxBytes: 16 * 1024 });
|
|
17770
|
+
} catch {
|
|
17771
|
+
return fail2(`the relay's realm document ${url} is not JSON`);
|
|
17772
|
+
}
|
|
17773
|
+
const { realm: _id, ...published } = typeof raw === "object" && raw !== null ? raw : {};
|
|
17774
|
+
const parsed = DirectoryRealmConfig.safeParse(published);
|
|
17775
|
+
if (!parsed.success)
|
|
17776
|
+
fail2(`the relay's realm document ${url} is not a directory realm`);
|
|
17777
|
+
const differs = realmMismatch(parsed.data, realm);
|
|
17778
|
+
if (differs.length > 0)
|
|
17779
|
+
fail2(`the relay's realm document ${url} names another ${differs.join(", ")} than the environment: the home is not pinned`);
|
|
17780
|
+
return parsed.data.orgDomains !== undefined ? { ...realm, orgDomains: parsed.data.orgDomains } : realm;
|
|
17781
|
+
}
|
|
17782
|
+
var JELLY_ENV, JELLY_RESOURCE_NAME = "messaging", ResourceEntry, Resources, wellKnownGet = async (url) => {
|
|
17783
|
+
const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(5000) });
|
|
17784
|
+
if (!response.ok)
|
|
17785
|
+
throw new MessagingError("relay_unavailable", `the relay answered ${response.status}`);
|
|
17786
|
+
const text = await response.text();
|
|
17787
|
+
if (Buffer.byteLength(text) > 16 * 1024)
|
|
17788
|
+
throw new MessagingError("too_large", "the relay's realm document is larger than 16 KiB");
|
|
17789
|
+
return text;
|
|
17790
|
+
};
|
|
17791
|
+
var init_jelly_env = __esm(() => {
|
|
17792
|
+
init_dist();
|
|
17793
|
+
init_zod();
|
|
17794
|
+
init_jelly3();
|
|
17795
|
+
JELLY_ENV = ["JELLY_ACTION_KEY", "JELLY_ID_ISSUER", "JELLY_RESOURCES", "JELLY_SERVICE_ANCHOR", "JELLY_DEVICE_LABEL", "JELLY_SOCKET_GROUP"];
|
|
17796
|
+
ResourceEntry = looseObject({ name: string2().max(200), resource: string2().min(1).max(2048), url: string2().min(1).max(2048) });
|
|
17797
|
+
Resources = array(unknown()).max(64);
|
|
17798
|
+
});
|
|
17799
|
+
|
|
17800
|
+
// packages/daemon/dist/consent/answer-preview.js
|
|
17801
|
+
function who3(from) {
|
|
17802
|
+
const name = cleanPeerText(from.name, { singleLine: true, maxChars: 80 }) || "someone";
|
|
17803
|
+
const email = from.email === undefined ? undefined : cleanPeerText(from.email, { singleLine: true, maxChars: 100 });
|
|
17804
|
+
return `${name} (${email !== undefined && email !== "" ? `${email}, ` : ""}${ORG[from.org]})`;
|
|
17805
|
+
}
|
|
17806
|
+
function answersFor(kind) {
|
|
17807
|
+
return kind === "suggestion" ? ["decline"] : kind === "contact" ? ["accept", "decline", "block"] : ["once", "always", "decline", "block"];
|
|
17808
|
+
}
|
|
17809
|
+
function answerSentence(input) {
|
|
17810
|
+
const { kind, answer } = input;
|
|
17811
|
+
if (answer !== undefined && !answersFor(kind).includes(answer)) {
|
|
17812
|
+
throw new MessagingError("invalid_request", `a ${kind === "message" || kind === "task" ? "request to your agent" : kind === "contact" ? "message request" : "suggestion"} takes ${answersFor(kind).join(", ")}`);
|
|
17813
|
+
}
|
|
17814
|
+
const sender = who3(input.from);
|
|
17815
|
+
const single = input.count <= 1;
|
|
17816
|
+
if (kind === "suggestion") {
|
|
17817
|
+
const email = cleanPeerText(input.email ?? "", { singleLine: true, maxChars: 100 });
|
|
17818
|
+
if (answer === undefined)
|
|
17819
|
+
return `Your agent ${input.agent} suggests you send a message request to ${email}.`;
|
|
17820
|
+
return `Dismiss your agent ${input.agent}'s suggestion to contact ${email}. Nothing is sent.`;
|
|
17821
|
+
}
|
|
17822
|
+
if (kind === "contact") {
|
|
17823
|
+
const thing = single ? "their message request" : `their ${input.count} message requests`;
|
|
17824
|
+
if (answer === undefined)
|
|
17825
|
+
return `${sender} asks to message you.`;
|
|
17826
|
+
if (answer === "accept")
|
|
17827
|
+
return `${sender} may message you from now on: ${single ? "their request becomes their first message" : "their requests become their first messages"}, and they get your card.`;
|
|
17828
|
+
if (answer === "decline")
|
|
17829
|
+
return `Decline ${thing} from ${sender}. They are told it was declined.`;
|
|
17830
|
+
return `Block ${sender}: nothing from them or their agents reaches you, and ${thing} ${single ? "is" : "are"} declined.`;
|
|
17831
|
+
}
|
|
17832
|
+
const thing = single ? `this ${kind}` : `these ${input.count} requests`;
|
|
17833
|
+
const src = single ? `${sender}'s agent ${input.agent}` : `${sender}'s agents`;
|
|
17834
|
+
const dst = single ? `your agent ${input.to}` : "your agents";
|
|
17835
|
+
if (answer === undefined)
|
|
17836
|
+
return `${src} asks to send ${kind === "task" ? "a task" : "a message"} to ${dst}.`;
|
|
17837
|
+
if (answer === "once")
|
|
17838
|
+
return `Deliver ${thing} from ${src} to ${dst} this once, guarded: ${single ? "your agent" : "your agents"} may answer only them.`;
|
|
17839
|
+
if (answer === "always")
|
|
17840
|
+
return `Deliver ${thing} from ${src} to ${dst}, and let their agents ask yours from now on without asking you.`;
|
|
17841
|
+
if (answer === "decline")
|
|
17842
|
+
return `Decline ${thing} from ${src}. They are told it was declined.`;
|
|
17843
|
+
return `Block ${sender}: nothing from them or their agents reaches you, and ${thing} ${single ? "is" : "are"} declined.`;
|
|
17844
|
+
}
|
|
17845
|
+
var ORG;
|
|
17846
|
+
var init_answer_preview = __esm(() => {
|
|
17847
|
+
init_dist();
|
|
17848
|
+
ORG = { same: "your org", other: "another org", none: "no org" };
|
|
17849
|
+
});
|
|
17850
|
+
|
|
17851
|
+
// packages/daemon/dist/snapshot.js
|
|
17852
|
+
import { createHash as createHash10 } from "crypto";
|
|
17853
|
+
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";
|
|
17854
|
+
import { join as join7 } from "path";
|
|
17855
|
+
function sha256File(path) {
|
|
17856
|
+
const data = readFileSync8(path);
|
|
17857
|
+
return { bytes: data.length, sha256: createHash10("sha256").update(data).digest("hex") };
|
|
17858
|
+
}
|
|
17859
|
+
|
|
17860
|
+
class Snapshots {
|
|
17861
|
+
#deps;
|
|
17862
|
+
#timer;
|
|
17863
|
+
constructor(deps) {
|
|
17864
|
+
this.#deps = deps;
|
|
17865
|
+
}
|
|
17866
|
+
get folder() {
|
|
17867
|
+
return join7(this.#deps.run, "snapshot");
|
|
17868
|
+
}
|
|
17869
|
+
take() {
|
|
17870
|
+
const folder = this.folder;
|
|
17871
|
+
if (existsSync6(folder))
|
|
17872
|
+
throw new MessagingError("invalid_state", "a snapshot is held: release it with daemon.snapshot {done: true} first");
|
|
17873
|
+
const group = this.#deps.group;
|
|
17874
|
+
mkdirSync5(folder, { mode: group === undefined ? 448 : 488 });
|
|
17875
|
+
try {
|
|
17876
|
+
if (!IS_WINDOWS && group !== undefined) {
|
|
17877
|
+
chownSync2(folder, process.geteuid?.() ?? -1, group);
|
|
17878
|
+
chmodSync4(folder, 488);
|
|
17879
|
+
}
|
|
17880
|
+
const fileMode = group === undefined ? 384 : 416;
|
|
17881
|
+
const files = [];
|
|
17882
|
+
const db = join7(folder, "messaging.db");
|
|
17883
|
+
this.#deps.vacuumInto(db);
|
|
17884
|
+
if (existsSync6(this.#deps.config))
|
|
17885
|
+
writeFileSync5(join7(folder, "config.json"), readFileSync8(this.#deps.config), { mode: fileMode });
|
|
17886
|
+
for (const name of ["messaging.db", "config.json"]) {
|
|
17887
|
+
const path = join7(folder, name);
|
|
17888
|
+
if (!existsSync6(path))
|
|
17889
|
+
continue;
|
|
17890
|
+
if (!IS_WINDOWS) {
|
|
17891
|
+
if (group !== undefined)
|
|
17892
|
+
chownSync2(path, process.geteuid?.() ?? -1, group);
|
|
17893
|
+
chmodSync4(path, fileMode);
|
|
17894
|
+
}
|
|
17895
|
+
files.push({ name, ...sha256File(path) });
|
|
17896
|
+
}
|
|
17897
|
+
const at = this.#deps.now();
|
|
17898
|
+
const until = at + SNAPSHOT_HOLD_MS;
|
|
17899
|
+
const manifest = { storeSchema: this.#deps.storeSchema, at, files };
|
|
17900
|
+
writeFileSync5(join7(folder, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
17901
|
+
`, { mode: fileMode });
|
|
17902
|
+
if (!IS_WINDOWS && group !== undefined)
|
|
17903
|
+
chownSync2(join7(folder, "manifest.json"), process.geteuid?.() ?? -1, group);
|
|
17904
|
+
clearTimeout(this.#timer);
|
|
17905
|
+
this.#timer = setTimeout(() => this.release(), SNAPSHOT_HOLD_MS);
|
|
17906
|
+
this.#timer.unref();
|
|
17907
|
+
return { folder, files, storeSchema: this.#deps.storeSchema, at, until };
|
|
17908
|
+
} catch (error) {
|
|
17909
|
+
this.release();
|
|
17910
|
+
if (error instanceof MessagingError)
|
|
17911
|
+
throw error;
|
|
17912
|
+
throw new MessagingError("internal", `the snapshot failed: ${error.code ?? "error"}`);
|
|
17913
|
+
}
|
|
17914
|
+
}
|
|
17915
|
+
release() {
|
|
17916
|
+
clearTimeout(this.#timer);
|
|
17917
|
+
this.#timer = undefined;
|
|
17918
|
+
let present;
|
|
17919
|
+
try {
|
|
17920
|
+
lstatSync4(this.folder);
|
|
17921
|
+
present = true;
|
|
17922
|
+
} catch {
|
|
17923
|
+
present = false;
|
|
17924
|
+
}
|
|
17925
|
+
if (present)
|
|
17926
|
+
rmSync4(this.folder, { recursive: true, force: true });
|
|
17927
|
+
return { released: present };
|
|
17928
|
+
}
|
|
17929
|
+
close() {
|
|
17930
|
+
this.release();
|
|
17931
|
+
}
|
|
17932
|
+
}
|
|
17933
|
+
var SNAPSHOT_HOLD_MS;
|
|
17934
|
+
var init_snapshot = __esm(() => {
|
|
17935
|
+
init_dist();
|
|
17936
|
+
init_dist2();
|
|
17937
|
+
SNAPSHOT_HOLD_MS = 60 * 60000;
|
|
17938
|
+
});
|
|
17939
|
+
|
|
17188
17940
|
// packages/daemon/dist/notices.js
|
|
17189
17941
|
class Notices {
|
|
17190
17942
|
#host;
|
|
@@ -17206,6 +17958,12 @@ class Notices {
|
|
|
17206
17958
|
alert(kind, at) {
|
|
17207
17959
|
this.#file({ id: `a_${kind.replace(/_/g, "")}${at}`, category: "alert", source: kind, at });
|
|
17208
17960
|
}
|
|
17961
|
+
device(seq, at) {
|
|
17962
|
+
this.#file({ id: `a_dev${seq}`, category: "alert", source: `device:${seq}`, at });
|
|
17963
|
+
}
|
|
17964
|
+
trust(grantId, person, until, at) {
|
|
17965
|
+
this.#file({ id: `a_trust${grantId}`, category: "alert", person, source: `trust:${until}`, at });
|
|
17966
|
+
}
|
|
17209
17967
|
#file(row) {
|
|
17210
17968
|
if (!this.#host.store.putNotice(row))
|
|
17211
17969
|
return;
|
|
@@ -17271,7 +18029,15 @@ class Notices {
|
|
|
17271
18029
|
const own = row.person === this.#host.me();
|
|
17272
18030
|
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" };
|
|
17273
18031
|
}
|
|
17274
|
-
if (row.category === "alert")
|
|
18032
|
+
if (row.category === "alert" && row.source.startsWith("device:")) {
|
|
18033
|
+
const device = this.#host.device?.(Number(row.source.slice("device:".length)));
|
|
18034
|
+
if (device)
|
|
18035
|
+
notice.device = device;
|
|
18036
|
+
notice.line = device ? deviceLine(device) : "A device of your account changed.";
|
|
18037
|
+
} else if (row.category === "alert" && row.source.startsWith("trust:")) {
|
|
18038
|
+
const until = Number(row.source.slice("trust:".length));
|
|
18039
|
+
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.`;
|
|
18040
|
+
} else if (row.category === "alert")
|
|
17275
18041
|
notice.line = ALERT_TEXT[row.source] ?? "An alarm was raised on this device.";
|
|
17276
18042
|
else if (row.category === "message" && lines) {
|
|
17277
18043
|
const line = this.#host.store.line(row.source);
|
|
@@ -17281,6 +18047,18 @@ class Notices {
|
|
|
17281
18047
|
return notice;
|
|
17282
18048
|
}
|
|
17283
18049
|
}
|
|
18050
|
+
function vouchWords2(d) {
|
|
18051
|
+
if (d.vouch === "admin")
|
|
18052
|
+
return "admitted by the team's admin";
|
|
18053
|
+
if (d.vouch === "device")
|
|
18054
|
+
return d.approver !== undefined ? `approved on '${d.approver}'` : "approved by link";
|
|
18055
|
+
return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID on a Jelly host";
|
|
18056
|
+
}
|
|
18057
|
+
function deviceLine(d) {
|
|
18058
|
+
const what = `'${d.label}'${d.kind !== undefined ? ` (${d.kind})` : ""}`;
|
|
18059
|
+
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.`;
|
|
18060
|
+
return line.length <= NOTICE_LINE_CHARS ? line : `${line.slice(0, NOTICE_LINE_CHARS - 1)}\u2026`;
|
|
18061
|
+
}
|
|
17284
18062
|
function firstLine(text) {
|
|
17285
18063
|
const first = cleanPeerText(text.split(/\r\n|\n|\r/, 1)[0] ?? "", { singleLine: true });
|
|
17286
18064
|
if (first.length <= NOTICE_LINE_CHARS)
|
|
@@ -17861,7 +18639,7 @@ var init_storage_errors = __esm(() => {
|
|
|
17861
18639
|
});
|
|
17862
18640
|
|
|
17863
18641
|
// packages/daemon/dist/store.js
|
|
17864
|
-
import { chmodSync as
|
|
18642
|
+
import { chmodSync as chmodSync5, closeSync as closeSync7, openSync as openSync3 } from "fs";
|
|
17865
18643
|
import { DatabaseSync } from "node:sqlite";
|
|
17866
18644
|
function additiveColumn(decl) {
|
|
17867
18645
|
if (/\b(PRIMARY|UNIQUE|CHECK|REFERENCES|COLLATE|GENERATED|AS)\b/i.test(decl))
|
|
@@ -17950,7 +18728,7 @@ class Store {
|
|
|
17950
18728
|
constructor(path) {
|
|
17951
18729
|
if (path !== ":memory:") {
|
|
17952
18730
|
closeSync7(openSync3(path, "a", 384));
|
|
17953
|
-
|
|
18731
|
+
chmodSync5(path, 384);
|
|
17954
18732
|
}
|
|
17955
18733
|
this.#db = new DatabaseSync(path, { timeout: 5000 });
|
|
17956
18734
|
this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;");
|
|
@@ -17965,6 +18743,11 @@ class Store {
|
|
|
17965
18743
|
this.#statements.clear();
|
|
17966
18744
|
this.#db.close();
|
|
17967
18745
|
}
|
|
18746
|
+
vacuumInto(path) {
|
|
18747
|
+
if (this.#depth > 0)
|
|
18748
|
+
throw new MessagingError("invalid_state", "a snapshot is not taken inside a transaction");
|
|
18749
|
+
this.#db.exec(`VACUUM INTO '${path.replaceAll("'", "''")}'`);
|
|
18750
|
+
}
|
|
17968
18751
|
tx(fn) {
|
|
17969
18752
|
if (this.#depth > 0) {
|
|
17970
18753
|
const name = `sp${this.#depth}`;
|
|
@@ -18602,6 +19385,14 @@ class Store {
|
|
|
18602
19385
|
SELECT substr(peer_address, 1, instr(peer_address || '@', '@') - 1), 0, created_at FROM tasks WHERE key = ?
|
|
18603
19386
|
) GROUP BY peer ORDER BY MAX(at) DESC, peer LIMIT ?`, key, key, key, limit).map((row) => ({ peer: String(row.peer), unread: Number(row.unread), lastAt: Number(row.at) }));
|
|
18604
19387
|
}
|
|
19388
|
+
awaitedRequests(sessions, terminal, limit) {
|
|
19389
|
+
if (sessions.length === 0)
|
|
19390
|
+
return [];
|
|
19391
|
+
const rows = this.#all(`SELECT request FROM tasks WHERE role = 'requester' AND request IS NOT NULL
|
|
19392
|
+
AND session IN (${sessions.map(() => "?").join(", ")}) AND state NOT IN (${terminal.map(() => "?").join(", ")})
|
|
19393
|
+
ORDER BY updated_at DESC, task_id DESC LIMIT ?`, ...sessions, ...terminal, limit);
|
|
19394
|
+
return rows.map((row) => String(row.request));
|
|
19395
|
+
}
|
|
18605
19396
|
task(taskId, role) {
|
|
18606
19397
|
const row = this.#get(`SELECT * FROM ${TASKS} WHERE task_id = ? AND role = ?`, taskId, role);
|
|
18607
19398
|
return row && taskRow2(row);
|
|
@@ -19173,14 +19964,14 @@ var init_version = __esm(() => {
|
|
|
19173
19964
|
});
|
|
19174
19965
|
|
|
19175
19966
|
// packages/daemon/dist/wake-bridge.js
|
|
19176
|
-
import { closeSync as closeSync8, constants as constants7, fstatSync as fstatSync4, lstatSync as
|
|
19967
|
+
import { closeSync as closeSync8, constants as constants7, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync4, readSync as readSync2 } from "fs";
|
|
19177
19968
|
import { request } from "http";
|
|
19178
19969
|
import { homedir as homedir2 } from "os";
|
|
19179
|
-
import { isAbsolute, join as
|
|
19970
|
+
import { isAbsolute as isAbsolute2, join as join8 } from "path";
|
|
19180
19971
|
function serviceFile(env = process.env, windows = IS_WINDOWS) {
|
|
19181
19972
|
const home = (windows ? env.USERPROFILE : env.HOME) || homedir2();
|
|
19182
|
-
const state = env.XDG_STATE_HOME &&
|
|
19183
|
-
return
|
|
19973
|
+
const state = env.XDG_STATE_HOME && isAbsolute2(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : join8(home, ".local", "state");
|
|
19974
|
+
return join8(state, "opencode", "service.json");
|
|
19184
19975
|
}
|
|
19185
19976
|
function loopbackOrigin(url) {
|
|
19186
19977
|
let parsed;
|
|
@@ -19199,7 +19990,7 @@ function loopbackOrigin(url) {
|
|
|
19199
19990
|
function readService(file) {
|
|
19200
19991
|
if (IS_WINDOWS) {
|
|
19201
19992
|
try {
|
|
19202
|
-
const stats =
|
|
19993
|
+
const stats = lstatSync5(file);
|
|
19203
19994
|
if (stats.isSymbolicLink() || !stats.isFile() || aclProblem(file, { pinnedOwner: false }) !== undefined)
|
|
19204
19995
|
return "service_unsafe";
|
|
19205
19996
|
} catch (error) {
|
|
@@ -19492,6 +20283,16 @@ class Grants {
|
|
|
19492
20283
|
hasGrant(person) {
|
|
19493
20284
|
return this.#current(grantSubjectKey({ person })) !== undefined;
|
|
19494
20285
|
}
|
|
20286
|
+
expiringTrust(withinMs) {
|
|
20287
|
+
const now = this.#host.now();
|
|
20288
|
+
const current = this.#currentDevice();
|
|
20289
|
+
return this.#all().flatMap(({ grant }) => {
|
|
20290
|
+
const until = grant.trustedUntil;
|
|
20291
|
+
if (!("person" in grant.subject) || until === undefined || until <= now || until > now + withinMs || !current(grant.by))
|
|
20292
|
+
return [];
|
|
20293
|
+
return [{ id: grant.id, person: grant.subject.person, until }];
|
|
20294
|
+
});
|
|
20295
|
+
}
|
|
19495
20296
|
list(p) {
|
|
19496
20297
|
const only = p.subject === undefined ? undefined : this.#subjectKey(p.subject, false);
|
|
19497
20298
|
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));
|
|
@@ -19566,6 +20367,25 @@ class Grants {
|
|
|
19566
20367
|
this.#host.log("grant.sync", { id: env.id, what: conv.what, merged });
|
|
19567
20368
|
this.sweep();
|
|
19568
20369
|
}
|
|
20370
|
+
resignFrom(predecessor) {
|
|
20371
|
+
const { identity } = this.#host.joined();
|
|
20372
|
+
const now = this.#host.now();
|
|
20373
|
+
const resigned = [];
|
|
20374
|
+
for (const { subject, grant } of this.#all()) {
|
|
20375
|
+
if (grant.by !== predecessor)
|
|
20376
|
+
continue;
|
|
20377
|
+
const copy = resignGrant(identity, grant, { now, keep: "all" });
|
|
20378
|
+
this.#store.putGrant(subject, copy);
|
|
20379
|
+
resigned.push(copy);
|
|
20380
|
+
if ("person" in copy.subject)
|
|
20381
|
+
this.#host.changed?.(copy.subject.person);
|
|
20382
|
+
}
|
|
20383
|
+
if (resigned.length > 0) {
|
|
20384
|
+
this.#sync("grant", resigned);
|
|
20385
|
+
this.#host.log("grant.resigned", { grants: resigned.length });
|
|
20386
|
+
}
|
|
20387
|
+
return resigned.length;
|
|
20388
|
+
}
|
|
19569
20389
|
sweep() {
|
|
19570
20390
|
const { identity, realm } = this.#host.joined();
|
|
19571
20391
|
if (realm.removed())
|
|
@@ -20211,6 +21031,8 @@ class Daemon {
|
|
|
20211
21031
|
#closed = false;
|
|
20212
21032
|
#online = new Set;
|
|
20213
21033
|
#holders = new Map;
|
|
21034
|
+
#ticketEnds = new Map;
|
|
21035
|
+
#lapsed = new Map;
|
|
20214
21036
|
#policy = {
|
|
20215
21037
|
rate: inboundRateLimiter(),
|
|
20216
21038
|
presence: responseRateLimiter(),
|
|
@@ -20230,6 +21052,9 @@ class Daemon {
|
|
|
20230
21052
|
#requests;
|
|
20231
21053
|
#notices;
|
|
20232
21054
|
#jellyHost;
|
|
21055
|
+
#attached = false;
|
|
21056
|
+
#wakeArmed = false;
|
|
21057
|
+
#snapshots;
|
|
20233
21058
|
#jellyAuth;
|
|
20234
21059
|
#state = "starting";
|
|
20235
21060
|
#sync;
|
|
@@ -20340,6 +21165,14 @@ class Daemon {
|
|
|
20340
21165
|
address: (sender, session) => peerRef(sender, session).address,
|
|
20341
21166
|
held: (row) => this.#notices.held(row)
|
|
20342
21167
|
});
|
|
21168
|
+
this.#snapshots = new Snapshots({
|
|
21169
|
+
run: paths.run,
|
|
21170
|
+
config: paths.config,
|
|
21171
|
+
vacuumInto: (path) => this.#store.vacuumInto(path),
|
|
21172
|
+
storeSchema: STORE_SCHEMA,
|
|
21173
|
+
group: options.jellyHost?.socketGroup,
|
|
21174
|
+
now: this.#now
|
|
21175
|
+
});
|
|
20343
21176
|
this.#notices = new Notices({
|
|
20344
21177
|
store,
|
|
20345
21178
|
now: this.#now,
|
|
@@ -20348,6 +21181,7 @@ class Daemon {
|
|
|
20348
21181
|
const realm = this.#realm ?? this.#directory?.realm;
|
|
20349
21182
|
return realm ? await realm.profile(person) : undefined;
|
|
20350
21183
|
},
|
|
21184
|
+
device: (seq) => this.#noticeDevice(seq),
|
|
20351
21185
|
emit: (event) => this.#emit(event)
|
|
20352
21186
|
});
|
|
20353
21187
|
this.#keyring = new Keyring({
|
|
@@ -20379,6 +21213,10 @@ class Daemon {
|
|
|
20379
21213
|
static async start(options) {
|
|
20380
21214
|
const paths = homePaths(options.home, options.jellyHost?.run);
|
|
20381
21215
|
ensureHome(paths);
|
|
21216
|
+
const jelly = options.jellyHost;
|
|
21217
|
+
if (jelly?.realm !== undefined && readConfig(paths.config).realm === undefined) {
|
|
21218
|
+
options = { ...options, jellyHost: { ...jelly, realm: await pinRealm(jelly.realm, options.wellKnown) } };
|
|
21219
|
+
}
|
|
20382
21220
|
jellyConfig(paths.config, options.jellyHost);
|
|
20383
21221
|
const lock = acquireLock(paths.lock);
|
|
20384
21222
|
let store;
|
|
@@ -20417,6 +21255,7 @@ class Daemon {
|
|
|
20417
21255
|
}
|
|
20418
21256
|
this.#sync.load();
|
|
20419
21257
|
this.#store.keepAllLive(this.#now(), CLI_AGENT);
|
|
21258
|
+
this.#snapshots.release();
|
|
20420
21259
|
const handlers = this.#handlers();
|
|
20421
21260
|
const jelly = this.#jellyHost;
|
|
20422
21261
|
if (jelly)
|
|
@@ -20448,6 +21287,7 @@ class Daemon {
|
|
|
20448
21287
|
}
|
|
20449
21288
|
this.#log("info", "daemon.started", { pid: process.pid, version: VERSION, joined: identity !== undefined, store: identityStore?.kind, ...jelly ? { deployment: "jelly" } : {} });
|
|
20450
21289
|
this.#state = "running";
|
|
21290
|
+
this.#deviceNotices(true);
|
|
20451
21291
|
this.#startTransport();
|
|
20452
21292
|
this.#sweepTimer = setInterval(() => this.sweep(), this.#tuning.sweepMs);
|
|
20453
21293
|
this.#sweepTimer.unref();
|
|
@@ -20487,7 +21327,12 @@ class Daemon {
|
|
|
20487
21327
|
this.#alerts();
|
|
20488
21328
|
},
|
|
20489
21329
|
onDeliver: (raw) => this.#onDeliver(raw),
|
|
20490
|
-
state: (connected) => this.#onHostedState(connected)
|
|
21330
|
+
state: (connected) => this.#onHostedState(connected),
|
|
21331
|
+
replaceStep: (token) => this.#replaceWith(token),
|
|
21332
|
+
eraseStep: async (token) => {
|
|
21333
|
+
const done = await this.#eraseWith(token);
|
|
21334
|
+
return { person: done.person, device: this.#identity.machine, seq: done.seq };
|
|
21335
|
+
}
|
|
20491
21336
|
});
|
|
20492
21337
|
this.#directory = home;
|
|
20493
21338
|
const realm = home.realm;
|
|
@@ -20505,6 +21350,7 @@ class Daemon {
|
|
|
20505
21350
|
else
|
|
20506
21351
|
this.#presenceSoon();
|
|
20507
21352
|
});
|
|
21353
|
+
realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
|
|
20508
21354
|
this.#removed = realm.removed();
|
|
20509
21355
|
this.#transport = home.transport();
|
|
20510
21356
|
home.start();
|
|
@@ -20541,6 +21387,7 @@ class Daemon {
|
|
|
20541
21387
|
await server.close().catch(() => {});
|
|
20542
21388
|
await Promise.race([this.#flushing, new Promise((resolve) => setTimeout(resolve, 2000).unref())]);
|
|
20543
21389
|
this.#store.close();
|
|
21390
|
+
this.#snapshots.close();
|
|
20544
21391
|
} finally {
|
|
20545
21392
|
this.#lock.release();
|
|
20546
21393
|
this.#log("info", "daemon.stopped", { pid: process.pid });
|
|
@@ -20604,7 +21451,11 @@ class Daemon {
|
|
|
20604
21451
|
return {
|
|
20605
21452
|
"daemon.status": () => this.status(),
|
|
20606
21453
|
"device.status": () => this.#deviceStatus(),
|
|
20607
|
-
"device.bind": (p) =>
|
|
21454
|
+
"device.bind": async (p) => {
|
|
21455
|
+
if (this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#directory)
|
|
21456
|
+
return this.#replaceWith(p.token, p.label);
|
|
21457
|
+
return this.#homeDirectory("device.bind").bind(this.#jellyHost ? { ...p, label: this.#jellyHost.label, kind: this.#jellyHost.kind } : p);
|
|
21458
|
+
},
|
|
20608
21459
|
"device.list": () => this.#directory?.list() ?? this.#rosterDevices(),
|
|
20609
21460
|
"device.remove": (p) => this.#homeDirectory("device.remove").remove(p),
|
|
20610
21461
|
"device.link": (p) => this.#homeDirectory("device.link").link(p),
|
|
@@ -20617,6 +21468,10 @@ class Daemon {
|
|
|
20617
21468
|
"device.login.cancel": () => this.#homeDirectory("device.login.cancel").loginCancel(),
|
|
20618
21469
|
"device.approve.preview": (p) => this.#homeDirectory("device.approve.preview").approvePreview(p),
|
|
20619
21470
|
"device.approve": (p) => this.#homeDirectory("device.approve").approve(p),
|
|
21471
|
+
"identity.replace": (p) => this.#replace(p),
|
|
21472
|
+
"identity.replace.preview": (p) => this.#replacePreview(p),
|
|
21473
|
+
"directory.erase": (p) => this.#erase(p),
|
|
21474
|
+
"directory.erase.preview": (p) => this.#erasePreview(p),
|
|
20620
21475
|
"daemon.stop": () => this.#stop(),
|
|
20621
21476
|
"session.register": (p, ctx) => this.#register(p, ctx),
|
|
20622
21477
|
"session.update": (p, ctx) => this.#updateSession(this.#bound(p, ctx)),
|
|
@@ -20689,6 +21544,7 @@ class Daemon {
|
|
|
20689
21544
|
},
|
|
20690
21545
|
"request.list": async () => ({ requests: [...this.#holds.list().requests, ...await this.#requests.list()] }),
|
|
20691
21546
|
"request.answer": (p) => this.#requests.holds(p.id) ? this.#requests.answer(p) : this.#holds.answer(p),
|
|
21547
|
+
"request.answer.preview": (p) => this.#answerPreview(p),
|
|
20692
21548
|
"contact.request": (p) => this.#requests.request(p),
|
|
20693
21549
|
"contact.request.preview": (p) => this.#requests.preview(p),
|
|
20694
21550
|
"card.get": async (p) => {
|
|
@@ -20697,6 +21553,11 @@ class Daemon {
|
|
|
20697
21553
|
return { sent: await this.#cards.get(p.person) };
|
|
20698
21554
|
},
|
|
20699
21555
|
"daemon.health": () => this.#health(),
|
|
21556
|
+
"daemon.woke": () => this.#woke(),
|
|
21557
|
+
"wake.arm": (p) => this.#wakeArm(p),
|
|
21558
|
+
"wake.disarm": () => this.#wakeDisarm(),
|
|
21559
|
+
"daemon.snapshot": (p) => p.done ? this.#snapshots.release() : this.#snapshots.take(),
|
|
21560
|
+
"person.attached": (p) => this.#personAttached(p.attached),
|
|
20700
21561
|
"agents.list": () => this.#agentsList(),
|
|
20701
21562
|
"agents.read": (p) => this.#agentsRead(p),
|
|
20702
21563
|
"notice.subscribe": (p, ctx, authority) => this.#notices.subscribe(ctx, p, authority?.scope === "host" ? authority.expiresAt : undefined),
|
|
@@ -20711,10 +21572,30 @@ class Daemon {
|
|
|
20711
21572
|
const out = [];
|
|
20712
21573
|
for (const person of all) {
|
|
20713
21574
|
const org = realm.kind === "roster" ? "same" : await realm.profile(person).then((profile) => profile.org, () => "none");
|
|
20714
|
-
|
|
21575
|
+
const reach = this.#cards.reach(person, org === "same");
|
|
21576
|
+
const changedAt = this.#recentDeviceChanges(realm, person).at(0)?.at;
|
|
21577
|
+
out.push(changedAt === undefined ? reach : { ...reach, devicesChangedAt: changedAt });
|
|
20715
21578
|
}
|
|
20716
21579
|
return out;
|
|
20717
21580
|
}
|
|
21581
|
+
#recentDeviceChanges(realm, person) {
|
|
21582
|
+
const since = this.#now() - DEVICES_CHANGED_MS;
|
|
21583
|
+
const changes = realm.deviceChanges(person);
|
|
21584
|
+
const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
|
|
21585
|
+
return changes.filter((change) => change.change !== "vouched" && change.at > since).reverse().slice(0, 10).map((change) => {
|
|
21586
|
+
const approver = change.approver !== undefined ? labelOf(change.approver) : undefined;
|
|
21587
|
+
return {
|
|
21588
|
+
change: change.change,
|
|
21589
|
+
device: change.device,
|
|
21590
|
+
label: MachineLabel.safeParse(change.label).success ? change.label : "device",
|
|
21591
|
+
...change.kind !== undefined ? { kind: change.kind } : {},
|
|
21592
|
+
vouch: change.vouch === "service" ? "device" : change.vouch,
|
|
21593
|
+
...change.via !== undefined ? { via: change.via } : {},
|
|
21594
|
+
...approver !== undefined && MachineLabel.safeParse(approver).success ? { approver } : {},
|
|
21595
|
+
at: change.at
|
|
21596
|
+
};
|
|
21597
|
+
});
|
|
21598
|
+
}
|
|
20718
21599
|
async#grantSubject(p) {
|
|
20719
21600
|
const { realm } = this.#joined();
|
|
20720
21601
|
if ("person" in p.subject)
|
|
@@ -20815,6 +21696,7 @@ class Daemon {
|
|
|
20815
21696
|
now: this.#now
|
|
20816
21697
|
});
|
|
20817
21698
|
realm.on("devices-changed", () => this.#realmChanged());
|
|
21699
|
+
realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
|
|
20818
21700
|
return realm;
|
|
20819
21701
|
}
|
|
20820
21702
|
#admin() {
|
|
@@ -20893,14 +21775,39 @@ class Daemon {
|
|
|
20893
21775
|
if (!holders)
|
|
20894
21776
|
this.#holders.set(p.key, holders = new Set);
|
|
20895
21777
|
holders.add(ctx.id);
|
|
21778
|
+
if (ticket) {
|
|
21779
|
+
this.#ticketEnds.set(p.key, Math.max(this.#ticketEnds.get(p.key) ?? 0, ticket.exp * 1000));
|
|
21780
|
+
for (const id of this.#lapsed.get(p.key) ?? [])
|
|
21781
|
+
holders.add(id);
|
|
21782
|
+
this.#lapsed.delete(p.key);
|
|
21783
|
+
}
|
|
20896
21784
|
this.#sessionsChanged();
|
|
20897
21785
|
return { session: name, address: formatAddress(me.person, name, me.label) };
|
|
20898
21786
|
}
|
|
20899
21787
|
#bound(p, ctx) {
|
|
21788
|
+
const ends = this.#ticketEnds.get(p.key);
|
|
21789
|
+
const held = this.#holders.get(p.key)?.has(ctx.id) === true || this.#lapsed.get(p.key)?.has(ctx.id) === true;
|
|
21790
|
+
if (ends !== undefined && ends <= this.#now() && held) {
|
|
21791
|
+
throw new MessagingError("ticket_invalid", "the session ticket this session registered with has expired: register again with a fresh ticket from the host");
|
|
21792
|
+
}
|
|
20900
21793
|
if (!this.#holders.get(p.key)?.has(ctx.id))
|
|
20901
21794
|
throw new MessagingError("not_found", "this session key is not registered on this connection");
|
|
20902
21795
|
return p;
|
|
20903
21796
|
}
|
|
21797
|
+
#expireTickets(now) {
|
|
21798
|
+
let changed = false;
|
|
21799
|
+
for (const [key, ends] of this.#ticketEnds) {
|
|
21800
|
+
if (ends > now || !this.#holders.has(key))
|
|
21801
|
+
continue;
|
|
21802
|
+
this.#lapsed.set(key, this.#holders.get(key));
|
|
21803
|
+
this.#holders.delete(key);
|
|
21804
|
+
this.#log("info", "session.ticket.expired", {});
|
|
21805
|
+
if (this.#store.keepSession(key, now))
|
|
21806
|
+
changed = true;
|
|
21807
|
+
}
|
|
21808
|
+
if (changed)
|
|
21809
|
+
this.#sessionsChanged();
|
|
21810
|
+
}
|
|
20904
21811
|
#sessionOf(key) {
|
|
20905
21812
|
const session = this.#store.sessionByKey(key);
|
|
20906
21813
|
if (!session)
|
|
@@ -20923,6 +21830,8 @@ class Daemon {
|
|
|
20923
21830
|
}
|
|
20924
21831
|
#unregister(p) {
|
|
20925
21832
|
this.#holders.delete(p.key);
|
|
21833
|
+
this.#ticketEnds.delete(p.key);
|
|
21834
|
+
this.#lapsed.delete(p.key);
|
|
20926
21835
|
if (this.#store.keepSession(p.key, this.#now()))
|
|
20927
21836
|
this.#sessionsChanged();
|
|
20928
21837
|
return { ok: true };
|
|
@@ -20933,10 +21842,14 @@ class Daemon {
|
|
|
20933
21842
|
this.#chats.disconnected(ctx);
|
|
20934
21843
|
this.#notices.disconnected(ctx.id);
|
|
20935
21844
|
let changed = false;
|
|
21845
|
+
for (const [key, lapsed] of [...this.#lapsed])
|
|
21846
|
+
if (lapsed.delete(ctx.id) && lapsed.size === 0)
|
|
21847
|
+
this.#lapsed.delete(key);
|
|
20936
21848
|
for (const [key, holders] of [...this.#holders]) {
|
|
20937
21849
|
if (!holders.delete(ctx.id) || holders.size > 0)
|
|
20938
21850
|
continue;
|
|
20939
21851
|
this.#holders.delete(key);
|
|
21852
|
+
this.#ticketEnds.delete(key);
|
|
20940
21853
|
if (this.#store.sessionByKey(key)?.agent === CLI_AGENT)
|
|
20941
21854
|
continue;
|
|
20942
21855
|
if (this.#store.keepSession(key, this.#now()))
|
|
@@ -20984,7 +21897,8 @@ class Daemon {
|
|
|
20984
21897
|
to: { team: true },
|
|
20985
21898
|
payload: presencePayload(final ? this.#store.sessions().map((s) => ({ ...s, unregisteredAt: s.unregisteredAt ?? this.#now() })) : this.#store.sessions(), me.label, this.#store.next("presence"), this.#transport?.advertise?.(), {
|
|
20986
21899
|
wake: (s) => this.#wakes(s),
|
|
20987
|
-
share: new Set(this.#currentConfig().presence?.share ?? [])
|
|
21900
|
+
share: new Set(this.#currentConfig().presence?.share ?? []),
|
|
21901
|
+
away: this.#away()
|
|
20988
21902
|
}),
|
|
20989
21903
|
now: this.#now()
|
|
20990
21904
|
});
|
|
@@ -21267,8 +22181,22 @@ class Daemon {
|
|
|
21267
22181
|
async#peopleGet(p) {
|
|
21268
22182
|
const { realm, identity } = this.#joined();
|
|
21269
22183
|
const profile = await realm.profile(p.person);
|
|
21270
|
-
const
|
|
21271
|
-
|
|
22184
|
+
const fresh = this.#now() - (realm.kind === "directory" ? this.#tuning.presenceOnlineMs : this.#tuning.presenceMaxMs * 2);
|
|
22185
|
+
const away = (machine) => {
|
|
22186
|
+
if (machine === identity.machine)
|
|
22187
|
+
return this.#away();
|
|
22188
|
+
const row = this.#store.directoryRow(machine);
|
|
22189
|
+
return row !== undefined && row.receivedAt >= fresh && row.presence.away === true;
|
|
22190
|
+
};
|
|
22191
|
+
const devices = realm.known(p.person).devices.map((device) => ({
|
|
22192
|
+
device: device.machine,
|
|
22193
|
+
label: device.label,
|
|
22194
|
+
self: device.machine === identity.machine,
|
|
22195
|
+
...away(device.machine) ? { away: true } : {}
|
|
22196
|
+
}));
|
|
22197
|
+
const own = p.person === this.#ownPerson();
|
|
22198
|
+
const deviceChanges = own ? [] : this.#recentDeviceChanges(realm, p.person);
|
|
22199
|
+
return { person: p.person, ...profile, devices, ...deviceChanges.length > 0 ? { deviceChanges } : {} };
|
|
21272
22200
|
}
|
|
21273
22201
|
#seal(type, toMachine, session, payload, extra = {}) {
|
|
21274
22202
|
const { identity, realm, me } = this.#joined();
|
|
@@ -22143,6 +23071,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22143
23071
|
return;
|
|
22144
23072
|
this.#directory?.sweep();
|
|
22145
23073
|
const now = this.#now();
|
|
23074
|
+
this.#expireTickets(now);
|
|
22146
23075
|
let sessions = 0;
|
|
22147
23076
|
try {
|
|
22148
23077
|
this.#tx(() => {
|
|
@@ -22404,8 +23333,14 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22404
23333
|
await transport?.stop().catch(() => {});
|
|
22405
23334
|
}
|
|
22406
23335
|
#deviceStatus() {
|
|
22407
|
-
if (this.#directory)
|
|
22408
|
-
|
|
23336
|
+
if (this.#directory) {
|
|
23337
|
+
const status = this.#directory.status();
|
|
23338
|
+
return this.#store.getKv(PREDECESSOR_KEY) !== undefined && status.binding !== "removed" ? { ...status, binding: "replace" } : status;
|
|
23339
|
+
}
|
|
23340
|
+
if (this.#config.realm) {
|
|
23341
|
+
const person = this.#store.directoryCache().own().person;
|
|
23342
|
+
return { realm: "directory", binding: "none", ...person ? { person } : {} };
|
|
23343
|
+
}
|
|
22409
23344
|
const identity = this.#identity;
|
|
22410
23345
|
const me = identity ? this.#realm?.device(identity.machine) : undefined;
|
|
22411
23346
|
const binding = this.#removed ? "removed" : me ? "bound" : "none";
|
|
@@ -22416,6 +23351,86 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22416
23351
|
const devices = realm.known(me.person).devices.map((d) => ({ device: d.machine, label: d.label, vouched: "admin", current: true, self: d.machine === identity.machine }));
|
|
22417
23352
|
return { devices };
|
|
22418
23353
|
}
|
|
23354
|
+
#away() {
|
|
23355
|
+
return this.#jellyHost !== undefined && !this.#attached;
|
|
23356
|
+
}
|
|
23357
|
+
#personAttached(attached) {
|
|
23358
|
+
if (attached !== this.#attached) {
|
|
23359
|
+
this.#attached = attached;
|
|
23360
|
+
this.#log("info", "person.attached", { attached });
|
|
23361
|
+
this.#presenceSoon();
|
|
23362
|
+
}
|
|
23363
|
+
return { ok: true };
|
|
23364
|
+
}
|
|
23365
|
+
#woke() {
|
|
23366
|
+
this.#log("info", "daemon.woke", {});
|
|
23367
|
+
this.#transport?.retryNow?.();
|
|
23368
|
+
this.sweep();
|
|
23369
|
+
this.#presenceSoon();
|
|
23370
|
+
this.#wokeHook();
|
|
23371
|
+
return { ok: true };
|
|
23372
|
+
}
|
|
23373
|
+
#wokeHook() {
|
|
23374
|
+
if (!this.#wakeArmed)
|
|
23375
|
+
return;
|
|
23376
|
+
this.#wakeDisarm().catch((error) => this.#log("warn", "wake.disarm.failed", { code: codeOf(error) }));
|
|
23377
|
+
}
|
|
23378
|
+
async#wakeArm(p) {
|
|
23379
|
+
const home = this.#directory;
|
|
23380
|
+
if (!home)
|
|
23381
|
+
throw new MessagingError("invalid_state", "wake tickets are for a directory realm");
|
|
23382
|
+
if (home.person() === undefined)
|
|
23383
|
+
throw new MessagingError("invalid_state", "this device is not bound yet");
|
|
23384
|
+
const issuer = this.#config.realm?.issuer;
|
|
23385
|
+
if (issuer === undefined || new URL(p.url).origin !== new URL(issuer).origin)
|
|
23386
|
+
throw new MessagingError("invalid_request", "the wake URL is not on the realm's Jelly ID");
|
|
23387
|
+
const agents = [...new Set(p.agents)];
|
|
23388
|
+
const awaiting = this.#store.awaitedRequests(agents, TERMINAL_STATES, WAKE_AWAITING);
|
|
23389
|
+
const sent = await home.wake({ url: p.url, ticket: p.ticket, agents, awaiting });
|
|
23390
|
+
this.#wakeArmed = true;
|
|
23391
|
+
this.#log("info", "wake.arm", { relay: sent === "sent" ? "armed" : "pending", agents: agents.length, awaiting: awaiting.length });
|
|
23392
|
+
return { relay: sent === "sent" ? "armed" : "pending", awaiting: awaiting.length };
|
|
23393
|
+
}
|
|
23394
|
+
async#wakeDisarm() {
|
|
23395
|
+
const home = this.#directory;
|
|
23396
|
+
if (!home)
|
|
23397
|
+
throw new MessagingError("invalid_state", "wake tickets are for a directory realm");
|
|
23398
|
+
const sent = await home.wake("disarm");
|
|
23399
|
+
this.#wakeArmed = false;
|
|
23400
|
+
this.#log("info", "wake.disarm", { relay: sent === "sent" ? "disarmed" : "pending" });
|
|
23401
|
+
return { relay: sent === "sent" ? "disarmed" : "pending" };
|
|
23402
|
+
}
|
|
23403
|
+
async#answerPreview(p) {
|
|
23404
|
+
const { realm, me } = this.#joined();
|
|
23405
|
+
const now = this.#now();
|
|
23406
|
+
const profile = async (person) => {
|
|
23407
|
+
const found = await realm.profile(person).catch(() => {
|
|
23408
|
+
return;
|
|
23409
|
+
});
|
|
23410
|
+
return found ? { name: found.name.slice(0, 200), ...found.email !== undefined ? { email: found.email.slice(0, 320) } : {}, org: found.org } : { name: String(person).slice(0, 200), org: "none" };
|
|
23411
|
+
};
|
|
23412
|
+
const answer = p.answer;
|
|
23413
|
+
const ask = this.#store.held(p.id);
|
|
23414
|
+
if (ask && ask.exp > now) {
|
|
23415
|
+
const kind = ask.payload.kind;
|
|
23416
|
+
const count = p.all ? this.#store.helds(ask.person).filter((row) => row.exp > now).length : 1;
|
|
23417
|
+
const from = await profile(ask.person);
|
|
23418
|
+
const agent = ask.envelope.from.agent ?? "";
|
|
23419
|
+
return { kind, from, ...agent !== "" ? { agent } : {}, to: ask.session, count, sentence: answerSentence({ kind, from, agent, to: ask.session, answer, count }) };
|
|
23420
|
+
}
|
|
23421
|
+
const request = this.#store.contactRequest(p.id);
|
|
23422
|
+
if (request && request.exp > now) {
|
|
23423
|
+
const count = p.all ? this.#store.contactRequests(request.person).filter((row) => row.exp > now).length : 1;
|
|
23424
|
+
const from = await profile(request.person);
|
|
23425
|
+
return { kind: "contact", from, count, sentence: answerSentence({ kind: "contact", from, answer, count }) };
|
|
23426
|
+
}
|
|
23427
|
+
const suggestion = this.#store.contactSuggestion(p.id);
|
|
23428
|
+
if (suggestion) {
|
|
23429
|
+
const from = await profile(me.person);
|
|
23430
|
+
return { kind: "suggestion", from, agent: suggestion.session, count: 1, sentence: answerSentence({ kind: "suggestion", from, agent: suggestion.session, email: suggestion.email, answer, count: 1 }) };
|
|
23431
|
+
}
|
|
23432
|
+
throw new MessagingError("not_found", "no request with that id is held here");
|
|
23433
|
+
}
|
|
22419
23434
|
#me() {
|
|
22420
23435
|
return (this.#realm ?? this.#directory?.realm)?.self()?.person;
|
|
22421
23436
|
}
|
|
@@ -22462,6 +23477,232 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22462
23477
|
const { rows, more } = this.#store.conversation(session.key, p.conversation.slice("agent:".length), { before: p.before }, p.limit ?? 30);
|
|
22463
23478
|
return { kind: "entries", conversation: p.conversation, entries: rows.map(convEntry), more };
|
|
22464
23479
|
}
|
|
23480
|
+
#heldRealm(config) {
|
|
23481
|
+
if (this.#directory)
|
|
23482
|
+
return this.#directory.realm;
|
|
23483
|
+
const throwaway = generateIdentity();
|
|
23484
|
+
return new DirectoryIdentity({ config, identity: () => throwaway, link: () => {
|
|
23485
|
+
return;
|
|
23486
|
+
}, cache: memoryDirectoryCache(this.#store.directoryCache()), jelly: this.#jelly ?? jellyFromIssuer(config.issuer), now: this.#now });
|
|
23487
|
+
}
|
|
23488
|
+
#predecessor() {
|
|
23489
|
+
return this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(MACHINE_KEY) ?? this.#identity?.machine;
|
|
23490
|
+
}
|
|
23491
|
+
async#replacePreview(p) {
|
|
23492
|
+
const config = this.#config.realm;
|
|
23493
|
+
if (!config)
|
|
23494
|
+
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");
|
|
23495
|
+
const person = this.#store.directoryCache().own().person;
|
|
23496
|
+
if (!person)
|
|
23497
|
+
throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
|
|
23498
|
+
const realm = this.#heldRealm(config);
|
|
23499
|
+
const pred = this.#predecessor();
|
|
23500
|
+
const record = realm.ownDevices().find((d) => d.device === pred);
|
|
23501
|
+
if (!record)
|
|
23502
|
+
throw new MessagingError("not_found", "this home's device is not in its account's log");
|
|
23503
|
+
const pending = this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#identity !== undefined && this.#identity.machine !== pred;
|
|
23504
|
+
const bound = pending && realm.binding().state !== "none";
|
|
23505
|
+
const signIns = !pending ? 2 : !bound ? 2 : record.current ? 1 : 0;
|
|
23506
|
+
const alarm = this.#keysMissing ? "keys_missing" : realm.alarm()?.kind === "restored" ? "restored" : null;
|
|
23507
|
+
const label = record.label;
|
|
23508
|
+
const why = alarm === "keys_missing" ? " (its key files are missing)" : alarm === "restored" ? " (it runs from an older copy of its home)" : "";
|
|
23509
|
+
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}'.` : "");
|
|
23510
|
+
return {
|
|
23511
|
+
predecessor: {
|
|
23512
|
+
device: pred,
|
|
23513
|
+
label,
|
|
23514
|
+
kind: record.kind,
|
|
23515
|
+
vouch: record.vouch,
|
|
23516
|
+
...record.vouch === "jelly" ? { via: record.host !== undefined ? "host" : "browser" } : {},
|
|
23517
|
+
since: record.since,
|
|
23518
|
+
current: record.current
|
|
23519
|
+
},
|
|
23520
|
+
alarm,
|
|
23521
|
+
...pending ? { device: this.#identity.machine } : {},
|
|
23522
|
+
signIns,
|
|
23523
|
+
sentence: sentence.slice(0, 600)
|
|
23524
|
+
};
|
|
23525
|
+
}
|
|
23526
|
+
async#replace(p) {
|
|
23527
|
+
const config = this.#config.realm;
|
|
23528
|
+
if (!config)
|
|
23529
|
+
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");
|
|
23530
|
+
if (this.#removed || this.#directory?.realm.removed())
|
|
23531
|
+
throw new MessagingError("invalid_state", "this device was removed from its account; it cannot replace anything");
|
|
23532
|
+
const person = this.#store.directoryCache().own().person;
|
|
23533
|
+
if (!person)
|
|
23534
|
+
throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
|
|
23535
|
+
const pred = this.#predecessor();
|
|
23536
|
+
if (!pred)
|
|
23537
|
+
throw new MessagingError("no_identity", "this home names no device to replace");
|
|
23538
|
+
const record = this.#heldRealm(config).ownDevices().find((d) => d.device === pred);
|
|
23539
|
+
if (!record)
|
|
23540
|
+
throw new MessagingError("not_found", "this home's device is not in its account's log");
|
|
23541
|
+
if (this.#store.getKv(PREDECESSOR_KEY) === undefined || this.#identity === undefined || this.#identity.machine === pred) {
|
|
23542
|
+
if (!record.current)
|
|
23543
|
+
throw new MessagingError("invalid_state", "this home's device was removed from its account; it cannot be replaced");
|
|
23544
|
+
await this.#newKeys(pred, config);
|
|
23545
|
+
}
|
|
23546
|
+
return this.#replaceNext(p);
|
|
23547
|
+
}
|
|
23548
|
+
async#replaceNext(p) {
|
|
23549
|
+
const home = this.#directory;
|
|
23550
|
+
const me = this.#identity.machine;
|
|
23551
|
+
if (p.token !== undefined) {
|
|
23552
|
+
const done = await this.#replaceWith(p.token, p.label);
|
|
23553
|
+
if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
|
|
23554
|
+
return { state: "done", person: done.person, device: me, predecessor: done.predecessor, seq: done.seq };
|
|
23555
|
+
} else if (this.#replaceFinished()) {
|
|
23556
|
+
return this.#replaceDone();
|
|
23557
|
+
}
|
|
23558
|
+
if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
|
|
23559
|
+
return this.#replaceDone();
|
|
23560
|
+
if (this.#jellyHost || p.token !== undefined)
|
|
23561
|
+
return { state: "token", device: me };
|
|
23562
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY);
|
|
23563
|
+
const record = home.realm.ownDevices().find((d) => d.device === pred);
|
|
23564
|
+
const label = p.label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device";
|
|
23565
|
+
const kind = record && record.kind !== "jelly-host" ? record.kind : "laptop";
|
|
23566
|
+
const started = await home.signIn("replace", { label, kind });
|
|
23567
|
+
return { state: "sign-in", device: me, ...started };
|
|
23568
|
+
}
|
|
23569
|
+
#replaceFinished() {
|
|
23570
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY);
|
|
23571
|
+
if (pred === undefined)
|
|
23572
|
+
return true;
|
|
23573
|
+
const realm = this.#directory?.realm;
|
|
23574
|
+
if (!realm || realm.binding().state === "none")
|
|
23575
|
+
return false;
|
|
23576
|
+
return realm.ownDevices().find((d) => d.device === pred)?.current !== true;
|
|
23577
|
+
}
|
|
23578
|
+
#replaceDone() {
|
|
23579
|
+
const realm = this.#directory.realm;
|
|
23580
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(REPLACED_KEY);
|
|
23581
|
+
this.#store.deleteKv(PREDECESSOR_KEY);
|
|
23582
|
+
const me = this.#identity.machine;
|
|
23583
|
+
const binding = realm.binding();
|
|
23584
|
+
const record = pred !== undefined ? realm.ownDevices().find((d) => d.device === pred) : undefined;
|
|
23585
|
+
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 };
|
|
23586
|
+
}
|
|
23587
|
+
async#replaceWith(token, label) {
|
|
23588
|
+
const home = this.#directory;
|
|
23589
|
+
const pred = this.#store.getKv(PREDECESSOR_KEY);
|
|
23590
|
+
if (!home || pred === undefined)
|
|
23591
|
+
throw new MessagingError("invalid_state", "no replacement is pending: start one with identity.replace");
|
|
23592
|
+
const realm = home.realm;
|
|
23593
|
+
const me = this.#identity.machine;
|
|
23594
|
+
const record = realm.ownDevices().find((d) => d.device === pred);
|
|
23595
|
+
let seq = 0;
|
|
23596
|
+
let spent = false;
|
|
23597
|
+
if (realm.binding().state === "none") {
|
|
23598
|
+
const kind = this.#jellyHost?.kind ?? (record && record.kind !== "jelly-host" ? record.kind : "laptop");
|
|
23599
|
+
const done = await home.bind({ token, label: this.#jellyHost?.label ?? label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device", kind });
|
|
23600
|
+
seq = done.seq;
|
|
23601
|
+
spent = signInToken(token);
|
|
23602
|
+
this.#log("info", "identity.replace.bound", { seq });
|
|
23603
|
+
}
|
|
23604
|
+
await realm.awaitSelf();
|
|
23605
|
+
this.#tx(() => this.#grants.resignFrom(pred));
|
|
23606
|
+
let unbound = false;
|
|
23607
|
+
if (!spent && realm.ownDevices().find((d) => d.device === pred)?.current) {
|
|
23608
|
+
const done = await realm.unbind(pred, { reason: "replaced", token });
|
|
23609
|
+
seq = done.seq;
|
|
23610
|
+
unbound = true;
|
|
23611
|
+
this.#log("info", "identity.replace.done", { seq });
|
|
23612
|
+
}
|
|
23613
|
+
if (unbound || this.#replaceFinished()) {
|
|
23614
|
+
this.#store.setKv(REPLACED_KEY, pred);
|
|
23615
|
+
this.#store.deleteKv(PREDECESSOR_KEY);
|
|
23616
|
+
this.#store.deleteKv(REPLACE_LABEL_KEY);
|
|
23617
|
+
this.#emit({ type: "contacts" });
|
|
23618
|
+
}
|
|
23619
|
+
return { person: realm.binding().person, device: me, predecessor: pred, seq };
|
|
23620
|
+
}
|
|
23621
|
+
async#newKeys(pred, config) {
|
|
23622
|
+
const label = this.#heldRealm(config).ownDevices().find((d) => d.device === pred)?.label;
|
|
23623
|
+
this.#store.setKv(PREDECESSOR_KEY, pred);
|
|
23624
|
+
if (label !== undefined)
|
|
23625
|
+
this.#store.setKv(REPLACE_LABEL_KEY, label);
|
|
23626
|
+
const keys = { identity: generateIdentity(), retained: [] };
|
|
23627
|
+
let identityStore = this.#keyring.store;
|
|
23628
|
+
if (identityStore)
|
|
23629
|
+
identityStore.save(keys);
|
|
23630
|
+
else
|
|
23631
|
+
identityStore = saveNewIdentity(this.paths.identity, keys, identitySettings(this.#config), this.#storeDeps());
|
|
23632
|
+
this.#store.setKv(MACHINE_KEY, keys.identity.machine);
|
|
23633
|
+
this.#log("info", "identity.replace.keys", { store: identityStore.kind });
|
|
23634
|
+
const old = this.#directory;
|
|
23635
|
+
this.#directory = undefined;
|
|
23636
|
+
this.#realm = undefined;
|
|
23637
|
+
this.#transport = undefined;
|
|
23638
|
+
if (old) {
|
|
23639
|
+
await old.stop();
|
|
23640
|
+
this.#onHostedState(false);
|
|
23641
|
+
}
|
|
23642
|
+
const cache = this.#store.directoryCache();
|
|
23643
|
+
if (cache.alarm()?.kind === "restored")
|
|
23644
|
+
cache.setAlarm(undefined);
|
|
23645
|
+
for (const row of this.#store.outbox())
|
|
23646
|
+
if (row.env.from.device === pred)
|
|
23647
|
+
this.#store.deleteOutbox(row.id);
|
|
23648
|
+
this.#keysMissing = false;
|
|
23649
|
+
this.#keyring.adopt(keys, identityStore);
|
|
23650
|
+
this.#openDirectory(config, keys, identityStore);
|
|
23651
|
+
}
|
|
23652
|
+
#erasePreview(_p) {
|
|
23653
|
+
const home = this.#homeDirectory("directory.erase.preview");
|
|
23654
|
+
const binding = home.realm.binding();
|
|
23655
|
+
if (!binding.person || binding.state === "none" || binding.state === "removed")
|
|
23656
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
23657
|
+
const devices = home.realm.ownDevices().filter((d) => d.current);
|
|
23658
|
+
const me = this.#identity.machine;
|
|
23659
|
+
return {
|
|
23660
|
+
person: binding.person,
|
|
23661
|
+
devices: devices.map((d) => ({ device: d.device, label: d.label, kind: d.kind, self: d.device === me })),
|
|
23662
|
+
signIns: this.#jellyHost || binding.record?.host !== undefined ? 0 : 1,
|
|
23663
|
+
perDay: ERASURES_PER_DAY,
|
|
23664
|
+
lost: [
|
|
23665
|
+
`Every device of your account (${devices.length}, this one included) is removed from Messaging, and none can be bound again.`,
|
|
23666
|
+
"Your Jelly account can never be bound to Messaging again: the erasure is for good, and a new sign-in is refused.",
|
|
23667
|
+
"The directory deletes your name, email and handle, and every device record and key statement it held for you.",
|
|
23668
|
+
"Messages still queued at the relay for your devices are deleted.",
|
|
23669
|
+
"Your contacts see a deleted account, and drop the grants and cards they hold for you."
|
|
23670
|
+
],
|
|
23671
|
+
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."]
|
|
23672
|
+
};
|
|
23673
|
+
}
|
|
23674
|
+
async#erase(p) {
|
|
23675
|
+
const home = this.#homeDirectory("directory.erase");
|
|
23676
|
+
const binding = home.realm.binding();
|
|
23677
|
+
if (!binding.person || binding.state === "none" || binding.state === "removed")
|
|
23678
|
+
throw new MessagingError("no_identity", "this device is not bound to an account");
|
|
23679
|
+
if (binding.record?.vouch !== "jelly")
|
|
23680
|
+
throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
|
|
23681
|
+
if (p.token !== undefined) {
|
|
23682
|
+
const done = await this.#eraseWith(p.token);
|
|
23683
|
+
return { state: "done", person: done.person, devices: done.devices };
|
|
23684
|
+
}
|
|
23685
|
+
if (this.#jellyHost)
|
|
23686
|
+
throw new MessagingError("invalid_request", "under a Jelly host, directory.erase needs a fresh token for this device from its host (token)");
|
|
23687
|
+
const record = binding.record;
|
|
23688
|
+
const started = await home.signIn("erase", { label: record.label, kind: record.kind !== "jelly-host" ? record.kind : "laptop" });
|
|
23689
|
+
return { state: "sign-in", ...started };
|
|
23690
|
+
}
|
|
23691
|
+
async#eraseWith(token) {
|
|
23692
|
+
const home = this.#homeDirectory("directory.erase");
|
|
23693
|
+
let done;
|
|
23694
|
+
try {
|
|
23695
|
+
done = await home.realm.erase(token);
|
|
23696
|
+
} catch (error) {
|
|
23697
|
+
if (!(error instanceof MessagingError) || error.code !== "rate_limited")
|
|
23698
|
+
throw error;
|
|
23699
|
+
const wait = error.retryAfterMs;
|
|
23700
|
+
const when = typeof wait === "number" && wait > 0 ? `; try again in about ${Math.max(1, Math.ceil(wait / 60000))} min` : "; try again tomorrow";
|
|
23701
|
+
throw new MessagingError("rate_limited", `the directory allows ${ERASURES_PER_DAY} erasures per person a day${when}`);
|
|
23702
|
+
}
|
|
23703
|
+
this.#log("warn", "directory.erased", { devices: done.devices });
|
|
23704
|
+
return done;
|
|
23705
|
+
}
|
|
22465
23706
|
#removePreview(p) {
|
|
22466
23707
|
const entry = this.#homeDirectory("device.remove.preview").list().devices.find((d) => d.device === p.device);
|
|
22467
23708
|
if (!entry)
|
|
@@ -22475,6 +23716,93 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22475
23716
|
...entry.since !== undefined ? { since: entry.since } : {}
|
|
22476
23717
|
};
|
|
22477
23718
|
}
|
|
23719
|
+
#deviceLogGrew(persons) {
|
|
23720
|
+
try {
|
|
23721
|
+
this.#deviceNotices();
|
|
23722
|
+
} catch (error) {
|
|
23723
|
+
this.#log("error", "notice.device.failed", { code: codeOf(error) });
|
|
23724
|
+
}
|
|
23725
|
+
if (persons.some((person) => person !== this.#me()))
|
|
23726
|
+
this.#emit({ type: "contacts" });
|
|
23727
|
+
}
|
|
23728
|
+
#ownPerson() {
|
|
23729
|
+
return this.#directory?.person() ?? this.#me();
|
|
23730
|
+
}
|
|
23731
|
+
#deviceNotices(start = false) {
|
|
23732
|
+
const realm = this.#realm ?? this.#directory?.realm;
|
|
23733
|
+
const person = this.#ownPerson();
|
|
23734
|
+
const me = this.#identity?.machine;
|
|
23735
|
+
if (!realm || !person || !me)
|
|
23736
|
+
return;
|
|
23737
|
+
const changes = realm.deviceChanges(person);
|
|
23738
|
+
const head = changes.at(-1)?.seq ?? -1;
|
|
23739
|
+
let last = this.#store.getKv(DEVICE_NOTICES_KEY);
|
|
23740
|
+
if (last === undefined) {
|
|
23741
|
+
const admitted = changes.find((change) => change.device === me && change.change === "added");
|
|
23742
|
+
if (start || admitted === undefined) {
|
|
23743
|
+
this.#store.setKv(DEVICE_NOTICES_KEY, String(head));
|
|
23744
|
+
return;
|
|
23745
|
+
}
|
|
23746
|
+
last = String(admitted.seq);
|
|
23747
|
+
}
|
|
23748
|
+
const fresh = changes.filter((change) => change.seq > Number(last));
|
|
23749
|
+
if (fresh.length === 0)
|
|
23750
|
+
return;
|
|
23751
|
+
const replacers = new Set(fresh.filter((change) => change.change === "replaced").map((change) => change.by));
|
|
23752
|
+
for (const change of fresh) {
|
|
23753
|
+
if (change.device === me || change.by === me)
|
|
23754
|
+
continue;
|
|
23755
|
+
if (change.change === "added" && replacers.has(change.device))
|
|
23756
|
+
continue;
|
|
23757
|
+
this.#notices.device(change.seq, change.at);
|
|
23758
|
+
}
|
|
23759
|
+
this.#store.setKv(DEVICE_NOTICES_KEY, String(Math.max(head, Number(last))));
|
|
23760
|
+
}
|
|
23761
|
+
#noticeDevice(seq) {
|
|
23762
|
+
const realm = this.#realm ?? this.#directory?.realm;
|
|
23763
|
+
const person = this.#ownPerson();
|
|
23764
|
+
if (!realm || !person)
|
|
23765
|
+
return;
|
|
23766
|
+
const changes = realm.deviceChanges(person);
|
|
23767
|
+
const change = changes.find((c) => c.seq === seq);
|
|
23768
|
+
if (!change)
|
|
23769
|
+
return;
|
|
23770
|
+
const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
|
|
23771
|
+
const gone = (device) => changes.some((c) => c.device === device && (c.change === "removed" || c.change === "replaced"));
|
|
23772
|
+
const subject = change.change === "replaced" ? change.by : change.device;
|
|
23773
|
+
const latest = change.change === "replaced" ? changes.filter((c) => c.device === subject && c.change !== "removed" && c.change !== "replaced").at(-1) : change;
|
|
23774
|
+
const label = latest?.label ?? labelOf(subject) ?? "device";
|
|
23775
|
+
const current = !gone(subject);
|
|
23776
|
+
const view = {
|
|
23777
|
+
change: change.change,
|
|
23778
|
+
device: subject,
|
|
23779
|
+
label: MachineLabel.safeParse(label).success ? label : "device",
|
|
23780
|
+
...latest?.kind !== undefined ? { kind: latest.kind } : {},
|
|
23781
|
+
vouch: ((v) => v === "service" ? "device" : v)(latest?.vouch ?? change.vouch),
|
|
23782
|
+
...(latest?.via ?? change.via) !== undefined ? { via: latest?.via ?? change.via } : {},
|
|
23783
|
+
...change.approver !== undefined && labelOf(change.approver) !== undefined ? { approver: labelOf(change.approver) } : {},
|
|
23784
|
+
...change.change === "replaced" ? { replaces: { device: change.device, label: change.label } } : {},
|
|
23785
|
+
current,
|
|
23786
|
+
at: change.at
|
|
23787
|
+
};
|
|
23788
|
+
const remove = current ? this.#removeWith() : undefined;
|
|
23789
|
+
if (remove)
|
|
23790
|
+
view.remove = remove;
|
|
23791
|
+
return view;
|
|
23792
|
+
}
|
|
23793
|
+
#removeWith() {
|
|
23794
|
+
const home = this.#directory;
|
|
23795
|
+
if (!home)
|
|
23796
|
+
return this.#roster?.isAdmin(this.#identity?.machine ?? "") ? "team" : undefined;
|
|
23797
|
+
const self = home.list().devices.find((device) => device.self && device.current);
|
|
23798
|
+
if (!self || self.vouched !== "jelly")
|
|
23799
|
+
return;
|
|
23800
|
+
return self.via === "browser" ? "sign-in" : "token";
|
|
23801
|
+
}
|
|
23802
|
+
#trustNotices() {
|
|
23803
|
+
for (const grant of this.#grants.expiringTrust(TRUST_NOTICE_MS))
|
|
23804
|
+
this.#notices.trust(grant.id, grant.person, grant.until, this.#now());
|
|
23805
|
+
}
|
|
22478
23806
|
#alerts() {
|
|
22479
23807
|
const realmAlarm = this.#directory?.status().alarm;
|
|
22480
23808
|
const roster = this.#keysMissing ? "keys_missing" : this.#keyring.restored() ? "restored" : undefined;
|
|
@@ -22492,6 +23820,16 @@ ${row.receipt ? "receipt" : "envelope"}`;
|
|
|
22492
23820
|
}
|
|
22493
23821
|
if (realmAlarm)
|
|
22494
23822
|
this.#notices.alert(realmAlarm.kind, realmAlarm.at);
|
|
23823
|
+
this.#deviceNotices();
|
|
23824
|
+
if (this.#joinedOrUndefined())
|
|
23825
|
+
this.#trustNotices();
|
|
23826
|
+
}
|
|
23827
|
+
#joinedOrUndefined() {
|
|
23828
|
+
try {
|
|
23829
|
+
return this.#joined();
|
|
23830
|
+
} catch {
|
|
23831
|
+
return;
|
|
23832
|
+
}
|
|
22495
23833
|
}
|
|
22496
23834
|
#homeDirectory(method) {
|
|
22497
23835
|
if (this.#directory)
|
|
@@ -22834,6 +24172,22 @@ function jellyConfig(path, jelly) {
|
|
|
22834
24172
|
}
|
|
22835
24173
|
return;
|
|
22836
24174
|
}
|
|
24175
|
+
if (jelly.realm !== undefined) {
|
|
24176
|
+
if (config.realm === undefined) {
|
|
24177
|
+
if (config.relay !== undefined || config.person !== undefined) {
|
|
24178
|
+
throw new MessagingError("invalid_request", `${path} is a roster home (relay, person): a Jelly host's home is pinned to a directory realm; give the service a new STATE_DIRECTORY`);
|
|
24179
|
+
}
|
|
24180
|
+
if (config.ipc?.agentGroup !== undefined)
|
|
24181
|
+
throw new MessagingError("invalid_request", "deployment jelly shares its sockets with --socket-group, not ipc.agentGroup");
|
|
24182
|
+
writeConfig(path, { ...config, realm: jelly.realm, deployment: "jelly" });
|
|
24183
|
+
return;
|
|
24184
|
+
}
|
|
24185
|
+
const differs = realmMismatch(config.realm, jelly.realm);
|
|
24186
|
+
if (differs.length > 0) {
|
|
24187
|
+
const which = differs.map((member) => `${member} ${JSON.stringify(config.realm[member])} (the environment: ${JSON.stringify(jelly.realm[member])})`).join(", ");
|
|
24188
|
+
throw new MessagingError("invalid_request", `${path} pins another realm: ${which}. A home never changes realm, and the daemon does not rewrite it; restore the variables the home was made with, or give the service a new STATE_DIRECTORY`);
|
|
24189
|
+
}
|
|
24190
|
+
}
|
|
22837
24191
|
if (config.realm === undefined)
|
|
22838
24192
|
throw new MessagingError("invalid_request", "deployment jelly needs a directory realm: config.json has no realm (a Jelly host supervises no roster realm)");
|
|
22839
24193
|
if (config.ipc?.agentGroup !== undefined)
|
|
@@ -22844,7 +24198,15 @@ function jellyConfig(path, jelly) {
|
|
|
22844
24198
|
function isLineTarget(to) {
|
|
22845
24199
|
return to.startsWith("#") || !to.includes("/");
|
|
22846
24200
|
}
|
|
22847
|
-
|
|
24201
|
+
function signInToken(token) {
|
|
24202
|
+
try {
|
|
24203
|
+
const claims = parseWire(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
24204
|
+
return typeof claims === "object" && claims !== null && claims.src === "device";
|
|
24205
|
+
} catch {
|
|
24206
|
+
return false;
|
|
24207
|
+
}
|
|
24208
|
+
}
|
|
24209
|
+
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;
|
|
22848
24210
|
var init_daemon = __esm(() => {
|
|
22849
24211
|
init_dist();
|
|
22850
24212
|
init_dist2();
|
|
@@ -22861,7 +24223,11 @@ var init_daemon = __esm(() => {
|
|
|
22861
24223
|
init_policy();
|
|
22862
24224
|
init_roster_identity();
|
|
22863
24225
|
init_directory_home();
|
|
24226
|
+
init_directory_identity();
|
|
22864
24227
|
init_jelly3();
|
|
24228
|
+
init_jelly_env();
|
|
24229
|
+
init_answer_preview();
|
|
24230
|
+
init_snapshot();
|
|
22865
24231
|
init_notices();
|
|
22866
24232
|
init_resolve();
|
|
22867
24233
|
init_secrets();
|
|
@@ -22887,6 +24253,8 @@ var init_daemon = __esm(() => {
|
|
|
22887
24253
|
};
|
|
22888
24254
|
KEPT_MS = DAY3;
|
|
22889
24255
|
INPUT_TIMEOUT_MS = DAY3;
|
|
24256
|
+
TRUST_NOTICE_MS = 7 * DAY3;
|
|
24257
|
+
DEVICES_CHANGED_MS = 7 * DAY3;
|
|
22890
24258
|
DEFAULT_INVITE_TTL_MS = DAY3;
|
|
22891
24259
|
});
|
|
22892
24260
|
|
|
@@ -23483,6 +24851,9 @@ class DirectTransport {
|
|
|
23483
24851
|
presence(env) {
|
|
23484
24852
|
this.#relay.presence(env);
|
|
23485
24853
|
}
|
|
24854
|
+
retryNow() {
|
|
24855
|
+
this.#relay.retryNow?.();
|
|
24856
|
+
}
|
|
23486
24857
|
appendRoster(entry) {
|
|
23487
24858
|
return this.#relay.appendRoster(entry);
|
|
23488
24859
|
}
|
|
@@ -23584,6 +24955,11 @@ var init_pacing = __esm(() => {
|
|
|
23584
24955
|
PAIR_WINDOW_MS = 30 * DAY_MS5;
|
|
23585
24956
|
});
|
|
23586
24957
|
|
|
24958
|
+
// packages/relay/dist/directory/wakes.js
|
|
24959
|
+
var init_wakes = __esm(() => {
|
|
24960
|
+
init_dist();
|
|
24961
|
+
});
|
|
24962
|
+
|
|
23587
24963
|
// packages/relay/dist/directory/module.js
|
|
23588
24964
|
var DAY4, DIRECTORY_FRAMES;
|
|
23589
24965
|
var init_module = __esm(() => {
|
|
@@ -23592,9 +24968,30 @@ var init_module = __esm(() => {
|
|
|
23592
24968
|
init_feed();
|
|
23593
24969
|
init_jelly4();
|
|
23594
24970
|
init_pacing();
|
|
24971
|
+
init_wakes();
|
|
23595
24972
|
DAY4 = 24 * 60;
|
|
23596
|
-
DIRECTORY_FRAMES = new Set([
|
|
24973
|
+
DIRECTORY_FRAMES = new Set([
|
|
24974
|
+
"dir.bind",
|
|
24975
|
+
"dir.append",
|
|
24976
|
+
"dir.keys",
|
|
24977
|
+
"dir.sth",
|
|
24978
|
+
"dir.lookup",
|
|
24979
|
+
"dir.device",
|
|
24980
|
+
"dir.resolve",
|
|
24981
|
+
"dir.search",
|
|
24982
|
+
"dir.watch",
|
|
24983
|
+
"presence.watch",
|
|
24984
|
+
"dir.erase",
|
|
24985
|
+
"wake.arm",
|
|
24986
|
+
"wake.disarm"
|
|
24987
|
+
]);
|
|
23597
24988
|
});
|
|
24989
|
+
|
|
24990
|
+
// packages/relay/dist/directory/store.js
|
|
24991
|
+
var init_store4 = __esm(() => {
|
|
24992
|
+
init_dist();
|
|
24993
|
+
});
|
|
24994
|
+
|
|
23598
24995
|
// packages/relay/dist/server.js
|
|
23599
24996
|
var HIGH_WATER, MAX_BUFFERED;
|
|
23600
24997
|
var init_server2 = __esm(() => {
|
|
@@ -23605,6 +25002,7 @@ var init_server2 = __esm(() => {
|
|
|
23605
25002
|
init_pages();
|
|
23606
25003
|
init_store3();
|
|
23607
25004
|
init_module();
|
|
25005
|
+
init_store4();
|
|
23608
25006
|
HIGH_WATER = 1024 * 1024;
|
|
23609
25007
|
MAX_BUFFERED = 16 * 1024 * 1024;
|
|
23610
25008
|
});
|
|
@@ -23657,6 +25055,14 @@ class RelayClient {
|
|
|
23657
25055
|
if (!this.#ws && !this.#retryTimer)
|
|
23658
25056
|
this.#open();
|
|
23659
25057
|
}
|
|
25058
|
+
retryNow() {
|
|
25059
|
+
if (!this.#running || this.#ws || !this.#retryTimer)
|
|
25060
|
+
return;
|
|
25061
|
+
clearTimeout(this.#retryTimer);
|
|
25062
|
+
this.#retryTimer = undefined;
|
|
25063
|
+
this.#attempt = 0;
|
|
25064
|
+
this.#open();
|
|
25065
|
+
}
|
|
23660
25066
|
async stop() {
|
|
23661
25067
|
this.#running = false;
|
|
23662
25068
|
clearTimeout(this.#retryTimer);
|
|
@@ -23733,6 +25139,12 @@ class RelayClient {
|
|
|
23733
25139
|
presenceWatch(persons) {
|
|
23734
25140
|
return this.#dir("dir.ok", { t: "presence.watch", persons });
|
|
23735
25141
|
}
|
|
25142
|
+
wakeArm(arm) {
|
|
25143
|
+
return this.#dir("dir.ok", { t: "wake.arm", url: arm.url, ticket: arm.ticket, agents: arm.agents, awaiting: arm.awaiting });
|
|
25144
|
+
}
|
|
25145
|
+
wakeDisarm() {
|
|
25146
|
+
return this.#dir("dir.ok", { t: "wake.disarm" });
|
|
25147
|
+
}
|
|
23736
25148
|
dirErase(entries, token) {
|
|
23737
25149
|
return this.#dir("dir.ok", { t: "dir.erase", entries, token });
|
|
23738
25150
|
}
|
|
@@ -24310,6 +25722,7 @@ class RelayDirectoryLink {
|
|
|
24310
25722
|
#running = false;
|
|
24311
25723
|
#watched = [];
|
|
24312
25724
|
#presence = [];
|
|
25725
|
+
#wake;
|
|
24313
25726
|
#opening = new Set;
|
|
24314
25727
|
constructor(options) {
|
|
24315
25728
|
this.#options = options;
|
|
@@ -24351,6 +25764,8 @@ class RelayDirectoryLink {
|
|
|
24351
25764
|
client.dirWatch(this.#watched).catch(() => {});
|
|
24352
25765
|
if (this.#presence.length > 0)
|
|
24353
25766
|
client.presenceWatch(this.#presence).catch(() => {});
|
|
25767
|
+
if (this.#wake !== undefined)
|
|
25768
|
+
this.#sendWake(client, this.#wake).catch(() => {});
|
|
24354
25769
|
const sth = "sth" in state ? STH.safeParse(state.sth) : undefined;
|
|
24355
25770
|
pushes.state(true, sth?.success ? sth.data : undefined);
|
|
24356
25771
|
},
|
|
@@ -24403,6 +25818,9 @@ class RelayDirectoryLink {
|
|
|
24403
25818
|
throw shape();
|
|
24404
25819
|
return { seq: reply.seq, sth: reply.sth };
|
|
24405
25820
|
}
|
|
25821
|
+
async erase(entries, token) {
|
|
25822
|
+
parse5(DirOkFrame, await link((await this.#authed()).dirErase(entries, token)));
|
|
25823
|
+
}
|
|
24406
25824
|
async keys(statement) {
|
|
24407
25825
|
const reply = parse5(DirOkFrame, await link((await this.#authed()).dirKeys(statement)));
|
|
24408
25826
|
if (reply.sth === undefined)
|
|
@@ -24458,6 +25876,34 @@ class RelayDirectoryLink {
|
|
|
24458
25876
|
presence(env) {
|
|
24459
25877
|
this.#client?.presence(env);
|
|
24460
25878
|
}
|
|
25879
|
+
retryNow() {
|
|
25880
|
+
this.#client?.retryNow();
|
|
25881
|
+
}
|
|
25882
|
+
async wake(state) {
|
|
25883
|
+
this.#wake = state;
|
|
25884
|
+
const client = this.#connect();
|
|
25885
|
+
if (!client?.connected)
|
|
25886
|
+
return "pending";
|
|
25887
|
+
try {
|
|
25888
|
+
await this.#sendWake(client, state);
|
|
25889
|
+
return "sent";
|
|
25890
|
+
} catch (error) {
|
|
25891
|
+
if (isMessagingError(error) && error.code === "relay_unavailable")
|
|
25892
|
+
return "pending";
|
|
25893
|
+
throw error;
|
|
25894
|
+
}
|
|
25895
|
+
}
|
|
25896
|
+
async#sendWake(client, state) {
|
|
25897
|
+
try {
|
|
25898
|
+
parse5(DirOkFrame, await (state === "disarm" ? client.wakeDisarm() : client.wakeArm(state)));
|
|
25899
|
+
} catch (error) {
|
|
25900
|
+
if (!(isMessagingError(error) && error.code === "relay_unavailable") && this.#wake === state)
|
|
25901
|
+
this.#wake = undefined;
|
|
25902
|
+
throw error;
|
|
25903
|
+
}
|
|
25904
|
+
if (this.#wake === state)
|
|
25905
|
+
this.#wake = undefined;
|
|
25906
|
+
}
|
|
24461
25907
|
async presenceWatch(persons) {
|
|
24462
25908
|
this.#presence = [...persons];
|
|
24463
25909
|
const client = this.#connect();
|
|
@@ -24516,6 +25962,7 @@ var init_dist4 = __esm(() => {
|
|
|
24516
25962
|
init_log();
|
|
24517
25963
|
init_version();
|
|
24518
25964
|
init_jelly3();
|
|
25965
|
+
init_jelly_env();
|
|
24519
25966
|
init_store2();
|
|
24520
25967
|
init_dist();
|
|
24521
25968
|
});
|
|
@@ -25756,6 +27203,12 @@ var people = {
|
|
|
25756
27203
|
}
|
|
25757
27204
|
ctx.out.line(`${ctx.out.style.bold(inline(person.name, 64))} handle ${inline(person.handle, 64)} org ${person.org} vouched ${person.vouched}`);
|
|
25758
27205
|
ctx.out.lines(table(["DEVICE", "LABEL", ""], person.devices.map((d) => [d.device, inline(d.label, 32), d.self ? "this device" : ""]), ctx.out.style));
|
|
27206
|
+
if (person.deviceChanges !== undefined) {
|
|
27207
|
+
const now = ctx.now();
|
|
27208
|
+
ctx.out.line(`${ctx.out.style.yellow(`${inline(person.name, 64)}'s devices changed:`)}`);
|
|
27209
|
+
for (const change of person.deviceChanges)
|
|
27210
|
+
ctx.out.line(` ${change.change} '${inline(change.label, 32)}' (${vouchWords(change)}) ${ago(change.at, now)}`);
|
|
27211
|
+
}
|
|
25759
27212
|
return;
|
|
25760
27213
|
}
|
|
25761
27214
|
if (ctx.json) {
|
|
@@ -25872,8 +27325,9 @@ var notices = {
|
|
|
25872
27325
|
usage: "[--lines] [--ack]",
|
|
25873
27326
|
details: [
|
|
25874
27327
|
"Each line is a message for you (a DM or channel line from someone else, or your own agent's line to you),",
|
|
25875
|
-
"a request held for you to answer (`messaging requests`), or an alert about this device
|
|
25876
|
-
"
|
|
27328
|
+
"a request held for you to answer (`messaging requests`), or an alert: about this device, a device added to or",
|
|
27329
|
+
"removed from your account (with how to remove one that is not yours), or trust you gave that ends within 7 days.",
|
|
27330
|
+
"--lines adds a message's first line (peer text). --ack marks every notice shown as seen."
|
|
25877
27331
|
].join(`
|
|
25878
27332
|
`),
|
|
25879
27333
|
options: {
|
|
@@ -25900,13 +27354,25 @@ var notices = {
|
|
|
25900
27354
|
ago(n.at, now),
|
|
25901
27355
|
n.category,
|
|
25902
27356
|
n.from ? inline(n.from.name, 40) : "",
|
|
25903
|
-
n.conversation !== undefined ? inline(n.conversation, 60) : "",
|
|
27357
|
+
n.conversation !== undefined ? inline(n.conversation, 60) : n.device !== undefined ? `device ${n.device.device}` : "",
|
|
25904
27358
|
n.line !== undefined ? inline(n.line, 120) : ""
|
|
25905
27359
|
]), ctx.out.style));
|
|
27360
|
+
const removable = list.flatMap((n) => n.device?.remove !== undefined && n.device.current ? [n.device] : []);
|
|
27361
|
+
for (const d of removable) {
|
|
27362
|
+
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";
|
|
27363
|
+
ctx.out.note(`Not yours? '${inline(d.label, 32)}': ${how}`);
|
|
27364
|
+
}
|
|
25906
27365
|
if (values.ack === true)
|
|
25907
27366
|
ctx.out.line(ctx.out.style.dim(`${list.length} marked seen`));
|
|
25908
27367
|
}
|
|
25909
27368
|
};
|
|
27369
|
+
function vouchWords(d) {
|
|
27370
|
+
if (d.vouch === "admin")
|
|
27371
|
+
return "admitted by the team's admin";
|
|
27372
|
+
if (d.vouch === "device")
|
|
27373
|
+
return d.approver !== undefined ? `approved on '${inline(d.approver, 32)}'` : "approved by link";
|
|
27374
|
+
return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID";
|
|
27375
|
+
}
|
|
25910
27376
|
var DIRECTORY_COMMANDS = [status, who2, people, devices, notices];
|
|
25911
27377
|
|
|
25912
27378
|
// packages/daemon/dist/cli/commands/identity.js
|
|
@@ -26176,6 +27642,55 @@ function aborted2(signal) {
|
|
|
26176
27642
|
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
26177
27643
|
});
|
|
26178
27644
|
}
|
|
27645
|
+
async function showSignIn(ctx, started, open) {
|
|
27646
|
+
if (!ctx.json) {
|
|
27647
|
+
const { bold } = ctx.out.style;
|
|
27648
|
+
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:";
|
|
27649
|
+
ctx.out.line(lead);
|
|
27650
|
+
ctx.out.line(` ${started.verificationUriComplete}`);
|
|
27651
|
+
ctx.out.line(`and check that the page shows the code ${bold(started.userCode)} and the key ${bold(started.fingerprint)}.`);
|
|
27652
|
+
ctx.out.line(`(Or open ${started.verificationUri} and type the code.)`);
|
|
27653
|
+
}
|
|
27654
|
+
if (open) {
|
|
27655
|
+
const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUriComplete);
|
|
27656
|
+
if (!opened)
|
|
27657
|
+
ctx.out.note("Could not open a browser here; open the link on any device.");
|
|
27658
|
+
} else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
|
|
27659
|
+
ctx.out.note("Over SSH: open the link on your laptop or phone; the code ties it to this machine.");
|
|
27660
|
+
}
|
|
27661
|
+
}
|
|
27662
|
+
async function waitSignIn(ctx, client, started) {
|
|
27663
|
+
const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
|
|
27664
|
+
if (!ctx.json)
|
|
27665
|
+
ctx.out.note(`Waiting for your approval (the code expires in ${minutes} min; Ctrl-C cancels)...`);
|
|
27666
|
+
const interrupted = aborted2(ctx.io.signal);
|
|
27667
|
+
let state = { ...started, state: "pending" };
|
|
27668
|
+
while (state.state === "pending") {
|
|
27669
|
+
const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
|
|
27670
|
+
const next = await Promise.race([asked, interrupted.then(() => {
|
|
27671
|
+
return;
|
|
27672
|
+
})]);
|
|
27673
|
+
if (next === undefined) {
|
|
27674
|
+
asked.catch(() => {});
|
|
27675
|
+
await client.call("device.login.cancel", {}).catch(() => {});
|
|
27676
|
+
ctx.out.note("Cancelled.");
|
|
27677
|
+
return;
|
|
27678
|
+
}
|
|
27679
|
+
state = next;
|
|
27680
|
+
}
|
|
27681
|
+
if (state.state === "failed")
|
|
27682
|
+
throw new MessagingError(state.code, state.message);
|
|
27683
|
+
return state;
|
|
27684
|
+
}
|
|
27685
|
+
async function confirmed(ctx, yes, question, word = "y") {
|
|
27686
|
+
if (yes)
|
|
27687
|
+
return true;
|
|
27688
|
+
if (ctx.json)
|
|
27689
|
+
throw new UsageError("with --json there is no prompt: confirm with --yes");
|
|
27690
|
+
ctx.out.prompt(`${question} Type ${word} to confirm: `);
|
|
27691
|
+
const answer = (await readLine(ctx.io.stdin) ?? "").trim().toLowerCase();
|
|
27692
|
+
return word === "y" ? /^y(es)?$/.test(answer) : answer === word;
|
|
27693
|
+
}
|
|
26179
27694
|
var login = {
|
|
26180
27695
|
name: "login",
|
|
26181
27696
|
summary: "Sign this device in to your account through the browser (hosted realm)",
|
|
@@ -26219,45 +27734,15 @@ var login = {
|
|
|
26219
27734
|
ctx.out.json(started);
|
|
26220
27735
|
return;
|
|
26221
27736
|
}
|
|
26222
|
-
|
|
26223
|
-
const { bold } = ctx.out.style;
|
|
26224
|
-
ctx.out.line(started.purpose === "remove" ? "To remove that device, sign in at Jelly ID:" : "To sign this device in, open:");
|
|
26225
|
-
ctx.out.line(` ${started.verificationUriComplete}`);
|
|
26226
|
-
ctx.out.line(`and check that the page shows the code ${bold(started.userCode)} and the key ${bold(started.fingerprint)}.`);
|
|
26227
|
-
ctx.out.line(`(Or open ${started.verificationUri} and type the code.)`);
|
|
26228
|
-
}
|
|
26229
|
-
if (values.open === true) {
|
|
26230
|
-
const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUriComplete);
|
|
26231
|
-
if (!opened)
|
|
26232
|
-
ctx.out.note("Could not open a browser here; open the link on any device.");
|
|
26233
|
-
} else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
|
|
26234
|
-
ctx.out.note("Over SSH: open the link on your laptop or phone; the code ties it to this machine.");
|
|
26235
|
-
}
|
|
27737
|
+
await showSignIn(ctx, started, values.open === true);
|
|
26236
27738
|
const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
|
|
26237
27739
|
if (!wait) {
|
|
26238
27740
|
ctx.out.note(`The daemon waits ${minutes} min for your approval. See it with: messaging devices`);
|
|
26239
27741
|
return;
|
|
26240
27742
|
}
|
|
26241
|
-
|
|
26242
|
-
|
|
26243
|
-
|
|
26244
|
-
const interrupted = aborted2(signal);
|
|
26245
|
-
let state = { ...started, state: "pending" };
|
|
26246
|
-
while (state.state === "pending") {
|
|
26247
|
-
const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
|
|
26248
|
-
const next = await Promise.race([asked, interrupted.then(() => {
|
|
26249
|
-
return;
|
|
26250
|
-
})]);
|
|
26251
|
-
if (next === undefined) {
|
|
26252
|
-
asked.catch(() => {});
|
|
26253
|
-
await client.call("device.login.cancel", {}).catch(() => {});
|
|
26254
|
-
ctx.out.note("Cancelled.");
|
|
26255
|
-
return 130;
|
|
26256
|
-
}
|
|
26257
|
-
state = next;
|
|
26258
|
-
}
|
|
26259
|
-
if (state.state === "failed")
|
|
26260
|
-
throw new MessagingError(state.code, state.message);
|
|
27743
|
+
const state = await waitSignIn(ctx, client, started);
|
|
27744
|
+
if (state === undefined)
|
|
27745
|
+
return 130;
|
|
26261
27746
|
if (ctx.json) {
|
|
26262
27747
|
ctx.out.json(state);
|
|
26263
27748
|
return state.state === "done" ? undefined : 1;
|
|
@@ -26274,7 +27759,128 @@ var login = {
|
|
|
26274
27759
|
}
|
|
26275
27760
|
}
|
|
26276
27761
|
};
|
|
26277
|
-
var
|
|
27762
|
+
var replace = {
|
|
27763
|
+
name: "identity replace",
|
|
27764
|
+
summary: "Replace this home's device with new keys after a restore (hosted realm)",
|
|
27765
|
+
usage: "[--label <label>] [--open] [--yes]",
|
|
27766
|
+
details: [
|
|
27767
|
+
"For a home restored from a backup or a snapshot (alarm keys_missing or restored), or for a fresh key on purpose.",
|
|
27768
|
+
"This device gets a new key, binds it to your account and removes the old device (reason replaced): your",
|
|
27769
|
+
"conversations, grants and history stay, and tasks continue. Mail still queued at the relay for the old device is",
|
|
27770
|
+
"lost. Through the browser you approve two sign-ins at Jelly ID: one binds the new key, one removes the old."
|
|
27771
|
+
].join(`
|
|
27772
|
+
`),
|
|
27773
|
+
options: {
|
|
27774
|
+
label: { type: "string", value: "<label>", description: "The new device's label (default: the old one's)" },
|
|
27775
|
+
open: { type: "boolean", description: "Open each link in this machine's browser" },
|
|
27776
|
+
yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
|
|
27777
|
+
},
|
|
27778
|
+
args: { min: 0, max: 0 },
|
|
27779
|
+
async run(ctx, _positionals, values) {
|
|
27780
|
+
const label = stringValue(values, "label");
|
|
27781
|
+
const client = await ctx.client();
|
|
27782
|
+
const preview = await client.call("identity.replace.preview", {});
|
|
27783
|
+
if (!ctx.json)
|
|
27784
|
+
ctx.out.line(inline(preview.sentence, 600));
|
|
27785
|
+
if (!await confirmed(ctx, values.yes === true, `Replace '${inline(preview.predecessor.label, 32)}' (device ${preview.predecessor.device}) with this device?`)) {
|
|
27786
|
+
ctx.out.note("Not replaced.");
|
|
27787
|
+
return 1;
|
|
27788
|
+
}
|
|
27789
|
+
const params = label !== undefined ? { label: parseWith2(MachineLabel, label, "--label must be a lowercase label, e.g. desk") } : {};
|
|
27790
|
+
for (let step = 0;step < 3; step++) {
|
|
27791
|
+
const result = await client.call("identity.replace", params);
|
|
27792
|
+
if (result.state === "done") {
|
|
27793
|
+
if (ctx.json)
|
|
27794
|
+
ctx.out.json(result);
|
|
27795
|
+
else
|
|
27796
|
+
ctx.out.line(`Replaced: this device (${result.device}) replaces ${result.predecessor} on ${inline(result.person, 64)}.`);
|
|
27797
|
+
return;
|
|
27798
|
+
}
|
|
27799
|
+
if (result.state === "token") {
|
|
27800
|
+
if (ctx.json)
|
|
27801
|
+
ctx.out.json(result);
|
|
27802
|
+
else
|
|
27803
|
+
ctx.out.note(`This device (${result.device}) waits for its Jelly host to bind the new key; the host's app finishes the replacement.`);
|
|
27804
|
+
return;
|
|
27805
|
+
}
|
|
27806
|
+
await showSignIn(ctx, result, values.open === true);
|
|
27807
|
+
const state = await waitSignIn(ctx, client, result);
|
|
27808
|
+
if (state === undefined)
|
|
27809
|
+
return 130;
|
|
27810
|
+
if (state.state !== "done") {
|
|
27811
|
+
ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
|
|
27812
|
+
return 1;
|
|
27813
|
+
}
|
|
27814
|
+
if ((await client.call("device.status", {})).binding !== "replace") {
|
|
27815
|
+
const done = { state: "done", person: state.person, device: result.device, predecessor: preview.predecessor.device, seq: state.seq };
|
|
27816
|
+
if (ctx.json)
|
|
27817
|
+
ctx.out.json(done);
|
|
27818
|
+
else
|
|
27819
|
+
ctx.out.line(`Replaced: this device (${result.device}) replaces ${preview.predecessor.device} on ${inline(state.person, 64)}.`);
|
|
27820
|
+
return;
|
|
27821
|
+
}
|
|
27822
|
+
if (!ctx.json)
|
|
27823
|
+
ctx.out.note("The new key is bound. One more sign-in removes the old device.");
|
|
27824
|
+
}
|
|
27825
|
+
throw new MessagingError("internal", "the replacement did not finish; run messaging identity replace again");
|
|
27826
|
+
}
|
|
27827
|
+
};
|
|
27828
|
+
var erase = {
|
|
27829
|
+
name: "erase",
|
|
27830
|
+
summary: "Erase your account from Messaging, for good (hosted realm)",
|
|
27831
|
+
usage: "[--open] [--yes]",
|
|
27832
|
+
details: [
|
|
27833
|
+
"Removes every device of your account and deletes what the directory holds about you. Your Jelly account can",
|
|
27834
|
+
"never be bound to Messaging again. The directory allows 10 erasures per person a day. Shows what is lost and",
|
|
27835
|
+
"asks you to type erase, unless --yes. A device approved by link cannot erase; one signed in through the browser",
|
|
27836
|
+
"approves one more sign-in at Jelly ID."
|
|
27837
|
+
].join(`
|
|
27838
|
+
`),
|
|
27839
|
+
options: {
|
|
27840
|
+
open: { type: "boolean", description: "Open the link in this machine's browser" },
|
|
27841
|
+
yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
|
|
27842
|
+
},
|
|
27843
|
+
args: { min: 0, max: 0 },
|
|
27844
|
+
async run(ctx, _positionals, values) {
|
|
27845
|
+
const client = await ctx.client();
|
|
27846
|
+
const preview = await client.call("directory.erase.preview", {});
|
|
27847
|
+
if (!ctx.json) {
|
|
27848
|
+
ctx.out.line(`Erasing ${inline(preview.person, 64)} from Messaging. What is lost:`);
|
|
27849
|
+
for (const line of preview.lost)
|
|
27850
|
+
ctx.out.line(` - ${inline(line, 300)}`);
|
|
27851
|
+
ctx.out.line(`Devices removed: ${preview.devices.map((d) => `'${inline(d.label, 32)}'${d.self ? " (this one)" : ""}`).join(", ")}`);
|
|
27852
|
+
ctx.out.line("What stays:");
|
|
27853
|
+
for (const line of preview.kept)
|
|
27854
|
+
ctx.out.line(` - ${inline(line, 300)}`);
|
|
27855
|
+
ctx.out.note(`The directory allows ${preview.perDay} erasures per person a day.`);
|
|
27856
|
+
}
|
|
27857
|
+
if (!await confirmed(ctx, values.yes === true, "This cannot be undone.", "erase")) {
|
|
27858
|
+
ctx.out.note("Not erased.");
|
|
27859
|
+
return 1;
|
|
27860
|
+
}
|
|
27861
|
+
const result = await client.call("directory.erase", {});
|
|
27862
|
+
if (result.state === "done") {
|
|
27863
|
+
if (ctx.json)
|
|
27864
|
+
ctx.out.json(result);
|
|
27865
|
+
else
|
|
27866
|
+
ctx.out.line(`Erased: ${result.devices} device(s) removed; this account is gone from Messaging.`);
|
|
27867
|
+
return;
|
|
27868
|
+
}
|
|
27869
|
+
await showSignIn(ctx, result, values.open === true);
|
|
27870
|
+
const state = await waitSignIn(ctx, client, result);
|
|
27871
|
+
if (state === undefined)
|
|
27872
|
+
return 130;
|
|
27873
|
+
if (state.state !== "done") {
|
|
27874
|
+
ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
|
|
27875
|
+
return 1;
|
|
27876
|
+
}
|
|
27877
|
+
if (ctx.json)
|
|
27878
|
+
ctx.out.json({ state: "done", person: state.person, devices: preview.devices.length });
|
|
27879
|
+
else
|
|
27880
|
+
ctx.out.line(`Erased: ${preview.devices.length} device(s) removed; this account is gone from Messaging.`);
|
|
27881
|
+
}
|
|
27882
|
+
};
|
|
27883
|
+
var LOGIN_COMMANDS = [login, replace, erase];
|
|
26278
27884
|
|
|
26279
27885
|
// packages/daemon/dist/cli/commands/messages.js
|
|
26280
27886
|
init_dist();
|
|
@@ -27026,13 +28632,13 @@ var HANDLES_INTERRUPT = new Set(["listen", "login"]);
|
|
|
27026
28632
|
var GROUPS = {
|
|
27027
28633
|
daemon: "Run the local daemon, messagingd",
|
|
27028
28634
|
team: "Manage the team: create, invite, join, members, roles (only the CLI may do this)",
|
|
27029
|
-
identity: "Show, move or
|
|
28635
|
+
identity: "Show, move, rotate or replace this machine's keys (only the CLI may do this)",
|
|
27030
28636
|
chat: "Your DMs and channels: list, read, post, search"
|
|
27031
28637
|
};
|
|
27032
28638
|
var SECTIONS = [
|
|
27033
28639
|
["Daemon", ["daemon start", "daemon stop", "status"]],
|
|
27034
28640
|
["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"]],
|
|
27035
|
-
["Identity", ["identity show", "identity migrate", "identity rotate-enc"]],
|
|
28641
|
+
["Identity", ["identity show", "identity migrate", "identity rotate-enc", "identity replace", "erase"]],
|
|
27036
28642
|
["People", ["login", "people", "devices", "grants", "grant", "requests", "contact", "card", "notices"]],
|
|
27037
28643
|
["Messages", ["who", "send", "inbox", "read", "message-status"]],
|
|
27038
28644
|
["Chats", ["chat list", "chat read", "chat post", "chat search"]],
|
|
@@ -27276,7 +28882,13 @@ Runs the Messaging daemon in the foreground for one MESSAGING_HOME (default ~/.m
|
|
|
27276
28882
|
--version print the version, the store schema and the protocol (--json: as JSON); needs no home
|
|
27277
28883
|
--help print this help
|
|
27278
28884
|
|
|
27279
|
-
Under a Jelly host (spec \xA78.11):
|
|
28885
|
+
Under a Jelly host (spec \xA78.11), with no arguments: the environment start contract. JELLY_ACTION_KEY (the path of
|
|
28886
|
+
the host's public action key) starts it; STATE_DIRECTORY is the home, RUNTIME_DIRECTORY the socket folder,
|
|
28887
|
+
JELLY_DEVICE_LABEL the label, JELLY_SOCKET_GROUP the group, and JELLY_ID_ISSUER, JELLY_RESOURCES (the messaging entry)
|
|
28888
|
+
and JELLY_SERVICE_ANCHOR the realm, which the first start writes to config.json. Flags win: a start with
|
|
28889
|
+
--deployment jelly reads none of these; any other start with JELLY_* variables set is refused.
|
|
28890
|
+
|
|
28891
|
+
Or by flags:
|
|
27280
28892
|
--deployment jelly every human and admin call needs the host's action grant
|
|
27281
28893
|
--act-key <path> the host's public action key (an Ed25519 JWK); required
|
|
27282
28894
|
--label <label> this device's label, from the host; required
|
|
@@ -27336,7 +28948,20 @@ function versionText(json, info) {
|
|
|
27336
28948
|
return `messagingd ${info.version} (store schema ${info.storeSchema}, protocol ${info.protocol})
|
|
27337
28949
|
`;
|
|
27338
28950
|
}
|
|
27339
|
-
|
|
28951
|
+
function startSource(values, env, named) {
|
|
28952
|
+
const flags = Object.keys(values).length > 0;
|
|
28953
|
+
if (flags) {
|
|
28954
|
+
if (values["--deployment"] === "jelly" || named.length === 0)
|
|
28955
|
+
return "flags";
|
|
28956
|
+
return `${named[0]} is set, so the environment starts a Jelly host, but the flags start a self-host daemon; use one or the other`;
|
|
28957
|
+
}
|
|
28958
|
+
if ((env.JELLY_ACTION_KEY ?? "") !== "")
|
|
28959
|
+
return "environment";
|
|
28960
|
+
if (named.length > 0)
|
|
28961
|
+
return `${named[0]} is set without JELLY_ACTION_KEY: a Jelly start needs the host's action key`;
|
|
28962
|
+
return "self";
|
|
28963
|
+
}
|
|
28964
|
+
async function daemonMain(argv, env = process.env) {
|
|
27340
28965
|
const args = parseArgs2(argv);
|
|
27341
28966
|
if (typeof args === "string") {
|
|
27342
28967
|
process.stderr.write(`messagingd: ${args}
|
|
@@ -27354,7 +28979,23 @@ ${USAGE}`);
|
|
|
27354
28979
|
return 0;
|
|
27355
28980
|
}
|
|
27356
28981
|
const { values } = args;
|
|
27357
|
-
const
|
|
28982
|
+
const source = startSource(values, env, jellyEnvNamed(env));
|
|
28983
|
+
if (source !== "flags" && source !== "environment" && source !== "self") {
|
|
28984
|
+
process.stderr.write(`messagingd: ${source}
|
|
28985
|
+
`);
|
|
28986
|
+
return 2;
|
|
28987
|
+
}
|
|
28988
|
+
let fromEnv;
|
|
28989
|
+
if (source === "environment") {
|
|
28990
|
+
try {
|
|
28991
|
+
fromEnv = readJellyEnv(env);
|
|
28992
|
+
} catch (error) {
|
|
28993
|
+
process.stderr.write(`messagingd: ${error instanceof Error ? error.message : "the Jelly environment is invalid"}
|
|
28994
|
+
`);
|
|
28995
|
+
return 1;
|
|
28996
|
+
}
|
|
28997
|
+
}
|
|
28998
|
+
const home = fromEnv ? resolve4(fromEnv.home) : values["--home"] ? resolve4(values["--home"]) : messagingHome();
|
|
27358
28999
|
const level = process.env.MESSAGING_LOG_LEVEL;
|
|
27359
29000
|
const log = createLogger({
|
|
27360
29001
|
file: homePaths(home).log,
|
|
@@ -27363,7 +29004,14 @@ ${USAGE}`);
|
|
|
27363
29004
|
});
|
|
27364
29005
|
let daemon;
|
|
27365
29006
|
try {
|
|
27366
|
-
const jellyHost =
|
|
29007
|
+
const jellyHost = fromEnv ? {
|
|
29008
|
+
actKey: readActKey(fromEnv.actKeyPath),
|
|
29009
|
+
label: fromEnv.label,
|
|
29010
|
+
kind: "jelly-host",
|
|
29011
|
+
realm: fromEnv.realm,
|
|
29012
|
+
...fromEnv.socketGroup !== undefined ? { socketGroup: fromEnv.socketGroup } : {},
|
|
29013
|
+
...fromEnv.run !== undefined ? { run: resolve4(fromEnv.run) } : {}
|
|
29014
|
+
} : values["--deployment"] === "jelly" ? {
|
|
27367
29015
|
actKey: readActKey(resolve4(values["--act-key"])),
|
|
27368
29016
|
label: jellyLabel(values["--label"]),
|
|
27369
29017
|
kind: "jelly-host",
|
|
@@ -27371,6 +29019,8 @@ ${USAGE}`);
|
|
|
27371
29019
|
...values["--run"] !== undefined ? { run: resolve4(values["--run"]) } : {}
|
|
27372
29020
|
} : undefined;
|
|
27373
29021
|
daemon = await startDaemon2({ home, log, ...jellyHost ? { jellyHost } : {} });
|
|
29022
|
+
if (jellyHost)
|
|
29023
|
+
log("info", "daemon.start", { deployment: "jelly", source });
|
|
27374
29024
|
} catch (error) {
|
|
27375
29025
|
process.stderr.write(`messagingd: ${error instanceof Error ? error.message : "failed to start"}
|
|
27376
29026
|
`);
|