@meshline/sdk 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/channels/repository.d.ts +73 -0
- package/dist/channels/repository.d.ts.map +1 -0
- package/dist/channels/repository.js +367 -0
- package/dist/channels/repository.js.map +1 -0
- package/dist/client/account-operations.d.ts +41 -0
- package/dist/client/account-operations.d.ts.map +1 -0
- package/dist/client/account-operations.js +304 -0
- package/dist/client/account-operations.js.map +1 -0
- package/dist/client/conversations.d.ts +36 -0
- package/dist/client/conversations.d.ts.map +1 -0
- package/dist/client/conversations.js +144 -0
- package/dist/client/conversations.js.map +1 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +162 -0
- package/dist/client.js.map +1 -0
- package/dist/components/account.d.ts +38 -0
- package/dist/components/account.d.ts.map +1 -0
- package/dist/components/account.js +243 -0
- package/dist/components/account.js.map +1 -0
- package/dist/components/channel.d.ts +78 -0
- package/dist/components/channel.d.ts.map +1 -0
- package/dist/components/channel.js +475 -0
- package/dist/components/channel.js.map +1 -0
- package/dist/components/component.d.ts +49 -0
- package/dist/components/component.d.ts.map +1 -0
- package/dist/components/component.js +194 -0
- package/dist/components/component.js.map +1 -0
- package/dist/components/device.d.ts +60 -0
- package/dist/components/device.d.ts.map +1 -0
- package/dist/components/device.js +648 -0
- package/dist/components/device.js.map +1 -0
- package/dist/components/group.d.ts +141 -0
- package/dist/components/group.d.ts.map +1 -0
- package/dist/components/group.js +933 -0
- package/dist/components/group.js.map +1 -0
- package/dist/components/message.d.ts +63 -0
- package/dist/components/message.d.ts.map +1 -0
- package/dist/components/message.js +439 -0
- package/dist/components/message.js.map +1 -0
- package/dist/components/profile.d.ts +37 -0
- package/dist/components/profile.d.ts.map +1 -0
- package/dist/components/profile.js +232 -0
- package/dist/components/profile.js.map +1 -0
- package/dist/crypto/groups.d.ts +56 -0
- package/dist/crypto/groups.d.ts.map +1 -0
- package/dist/crypto/groups.js +163 -0
- package/dist/crypto/groups.js.map +1 -0
- package/dist/crypto/messages.d.ts +52 -0
- package/dist/crypto/messages.d.ts.map +1 -0
- package/dist/crypto/messages.js +180 -0
- package/dist/crypto/messages.js.map +1 -0
- package/dist/crypto/primitives.d.ts +30 -0
- package/dist/crypto/primitives.d.ts.map +1 -0
- package/dist/crypto/primitives.js +102 -0
- package/dist/crypto/primitives.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/groups/account-sync.d.ts +28 -0
- package/dist/groups/account-sync.d.ts.map +1 -0
- package/dist/groups/account-sync.js +235 -0
- package/dist/groups/account-sync.js.map +1 -0
- package/dist/groups/messages.d.ts +39 -0
- package/dist/groups/messages.d.ts.map +1 -0
- package/dist/groups/messages.js +141 -0
- package/dist/groups/messages.js.map +1 -0
- package/dist/groups/operations.d.ts +57 -0
- package/dist/groups/operations.d.ts.map +1 -0
- package/dist/groups/operations.js +225 -0
- package/dist/groups/operations.js.map +1 -0
- package/dist/groups/repository.d.ts +56 -0
- package/dist/groups/repository.d.ts.map +1 -0
- package/dist/groups/repository.js +254 -0
- package/dist/groups/repository.js.map +1 -0
- package/dist/groups/secrets.d.ts +67 -0
- package/dist/groups/secrets.d.ts.map +1 -0
- package/dist/groups/secrets.js +452 -0
- package/dist/groups/secrets.js.map +1 -0
- package/dist/groups/state.d.ts +39 -0
- package/dist/groups/state.d.ts.map +1 -0
- package/dist/groups/state.js +201 -0
- package/dist/groups/state.js.map +1 -0
- package/dist/identity/identifiers.d.ts +9 -0
- package/dist/identity/identifiers.d.ts.map +1 -0
- package/dist/identity/identifiers.js +37 -0
- package/dist/identity/identifiers.js.map +1 -0
- package/dist/identity/neo.d.ts +11 -0
- package/dist/identity/neo.d.ts.map +1 -0
- package/dist/identity/neo.js +77 -0
- package/dist/identity/neo.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions.d.ts +31 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +2 -0
- package/dist/interactions.js.map +1 -0
- package/dist/messages/account-sender.d.ts +9 -0
- package/dist/messages/account-sender.d.ts.map +1 -0
- package/dist/messages/account-sender.js +46 -0
- package/dist/messages/account-sender.js.map +1 -0
- package/dist/messages/contacts.d.ts +103 -0
- package/dist/messages/contacts.d.ts.map +1 -0
- package/dist/messages/contacts.js +525 -0
- package/dist/messages/contacts.js.map +1 -0
- package/dist/messages/outbox.d.ts +66 -0
- package/dist/messages/outbox.d.ts.map +1 -0
- package/dist/messages/outbox.js +167 -0
- package/dist/messages/outbox.js.map +1 -0
- package/dist/messages/receiver.d.ts +35 -0
- package/dist/messages/receiver.d.ts.map +1 -0
- package/dist/messages/receiver.js +113 -0
- package/dist/messages/receiver.js.map +1 -0
- package/dist/messages/repository.d.ts +89 -0
- package/dist/messages/repository.d.ts.map +1 -0
- package/dist/messages/repository.js +295 -0
- package/dist/messages/repository.js.map +1 -0
- package/dist/models/authentication.d.ts +37 -0
- package/dist/models/authentication.d.ts.map +1 -0
- package/dist/models/authentication.js +53 -0
- package/dist/models/authentication.js.map +1 -0
- package/dist/models/channels.d.ts +141 -0
- package/dist/models/channels.d.ts.map +1 -0
- package/dist/models/channels.js +258 -0
- package/dist/models/channels.js.map +1 -0
- package/dist/models/contacts.d.ts +68 -0
- package/dist/models/contacts.d.ts.map +1 -0
- package/dist/models/contacts.js +186 -0
- package/dist/models/contacts.js.map +1 -0
- package/dist/models/content.d.ts +37 -0
- package/dist/models/content.d.ts.map +1 -0
- package/dist/models/content.js +137 -0
- package/dist/models/content.js.map +1 -0
- package/dist/models/device-state.d.ts +8 -0
- package/dist/models/device-state.d.ts.map +1 -0
- package/dist/models/device-state.js +22 -0
- package/dist/models/device-state.js.map +1 -0
- package/dist/models/group-admission.d.ts +111 -0
- package/dist/models/group-admission.d.ts.map +1 -0
- package/dist/models/group-admission.js +153 -0
- package/dist/models/group-admission.js.map +1 -0
- package/dist/models/group-keys.d.ts +30 -0
- package/dist/models/group-keys.d.ts.map +1 -0
- package/dist/models/group-keys.js +52 -0
- package/dist/models/group-keys.js.map +1 -0
- package/dist/models/group-management.d.ts +190 -0
- package/dist/models/group-management.d.ts.map +1 -0
- package/dist/models/group-management.js +263 -0
- package/dist/models/group-management.js.map +1 -0
- package/dist/models/groups.d.ts +81 -0
- package/dist/models/groups.d.ts.map +1 -0
- package/dist/models/groups.js +127 -0
- package/dist/models/groups.js.map +1 -0
- package/dist/models/identity.d.ts +47 -0
- package/dist/models/identity.d.ts.map +1 -0
- package/dist/models/identity.js +136 -0
- package/dist/models/identity.js.map +1 -0
- package/dist/models/messages.d.ts +104 -0
- package/dist/models/messages.d.ts.map +1 -0
- package/dist/models/messages.js +168 -0
- package/dist/models/messages.js.map +1 -0
- package/dist/models/profile.d.ts +24 -0
- package/dist/models/profile.d.ts.map +1 -0
- package/dist/models/profile.js +47 -0
- package/dist/models/profile.js.map +1 -0
- package/dist/models/relay.d.ts +39 -0
- package/dist/models/relay.d.ts.map +1 -0
- package/dist/models/relay.js +160 -0
- package/dist/models/relay.js.map +1 -0
- package/dist/protocol/codec.d.ts +36 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +144 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/context.d.ts +14 -0
- package/dist/protocol/context.d.ts.map +1 -0
- package/dist/protocol/context.js +43 -0
- package/dist/protocol/context.js.map +1 -0
- package/dist/protocol/encoding.d.ts +15 -0
- package/dist/protocol/encoding.d.ts.map +1 -0
- package/dist/protocol/encoding.js +133 -0
- package/dist/protocol/encoding.js.map +1 -0
- package/dist/protocol/json.d.ts +16 -0
- package/dist/protocol/json.d.ts.map +1 -0
- package/dist/protocol/json.js +172 -0
- package/dist/protocol/json.js.map +1 -0
- package/dist/protocol/validation.d.ts +4 -0
- package/dist/protocol/validation.d.ts.map +1 -0
- package/dist/protocol/validation.js +18 -0
- package/dist/protocol/validation.js.map +1 -0
- package/dist/runtime/abort.d.ts +6 -0
- package/dist/runtime/abort.d.ts.map +1 -0
- package/dist/runtime/abort.js +32 -0
- package/dist/runtime/abort.js.map +1 -0
- package/dist/runtime/async-gate.d.ts +6 -0
- package/dist/runtime/async-gate.d.ts.map +1 -0
- package/dist/runtime/async-gate.js +15 -0
- package/dist/runtime/async-gate.js.map +1 -0
- package/dist/runtime/async-pulse.d.ts +7 -0
- package/dist/runtime/async-pulse.d.ts.map +1 -0
- package/dist/runtime/async-pulse.js +23 -0
- package/dist/runtime/async-pulse.js.map +1 -0
- package/dist/runtime/clock.d.ts +16 -0
- package/dist/runtime/clock.d.ts.map +1 -0
- package/dist/runtime/clock.js +62 -0
- package/dist/runtime/clock.js.map +1 -0
- package/dist/runtime/events.d.ts +11 -0
- package/dist/runtime/events.d.ts.map +1 -0
- package/dist/runtime/events.js +45 -0
- package/dist/runtime/events.js.map +1 -0
- package/dist/runtime/url.d.ts +3 -0
- package/dist/runtime/url.d.ts.map +1 -0
- package/dist/runtime/url.js +23 -0
- package/dist/runtime/url.js.map +1 -0
- package/dist/storage/signed-request.d.ts +14 -0
- package/dist/storage/signed-request.d.ts.map +1 -0
- package/dist/storage/signed-request.js +26 -0
- package/dist/storage/signed-request.js.map +1 -0
- package/dist/storage/store.d.ts +58 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +52 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/storage/transaction.d.ts +7 -0
- package/dist/storage/transaction.d.ts.map +1 -0
- package/dist/storage/transaction.js +22 -0
- package/dist/storage/transaction.js.map +1 -0
- package/dist/transport/client.d.ts +61 -0
- package/dist/transport/client.d.ts.map +1 -0
- package/dist/transport/client.js +392 -0
- package/dist/transport/client.js.map +1 -0
- package/dist/transport/discovery.d.ts +19 -0
- package/dist/transport/discovery.d.ts.map +1 -0
- package/dist/transport/discovery.js +75 -0
- package/dist/transport/discovery.js.map +1 -0
- package/dist/transport/endpoint.d.ts +6 -0
- package/dist/transport/endpoint.d.ts.map +1 -0
- package/dist/transport/endpoint.js +35 -0
- package/dist/transport/endpoint.js.map +1 -0
- package/dist/transport/http.d.ts +52 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +128 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/pool.d.ts +31 -0
- package/dist/transport/pool.d.ts.map +1 -0
- package/dist/transport/pool.js +137 -0
- package/dist/transport/pool.js.map +1 -0
- package/dist/transport/relay-error.d.ts +23 -0
- package/dist/transport/relay-error.d.ts.map +1 -0
- package/dist/transport/relay-error.js +47 -0
- package/dist/transport/relay-error.js.map +1 -0
- package/dist/transport/rpc.d.ts +31 -0
- package/dist/transport/rpc.d.ts.map +1 -0
- package/dist/transport/rpc.js +56 -0
- package/dist/transport/rpc.js.map +1 -0
- package/dist/transport/session.d.ts +49 -0
- package/dist/transport/session.d.ts.map +1 -0
- package/dist/transport/session.js +169 -0
- package/dist/transport/session.js.map +1 -0
- package/dist/transport/socket.d.ts +55 -0
- package/dist/transport/socket.d.ts.map +1 -0
- package/dist/transport/socket.js +266 -0
- package/dist/transport/socket.js.map +1 -0
- package/dist/transport/subscription.d.ts +36 -0
- package/dist/transport/subscription.d.ts.map +1 -0
- package/dist/transport/subscription.js +169 -0
- package/dist/transport/subscription.js.map +1 -0
- package/package.json +35 -4
- package/src/channels/repository.ts +244 -0
- package/src/client/account-operations.ts +183 -0
- package/src/client/conversations.ts +95 -0
- package/src/client.ts +108 -0
- package/src/components/account.ts +206 -0
- package/src/components/channel.ts +253 -0
- package/src/components/component.ts +151 -0
- package/src/components/device.ts +455 -0
- package/src/components/group.ts +546 -0
- package/src/components/message.ts +253 -0
- package/src/components/profile.ts +179 -0
- package/src/crypto/groups.ts +107 -0
- package/src/crypto/messages.ts +129 -0
- package/src/crypto/primitives.ts +103 -0
- package/src/errors.ts +13 -0
- package/src/groups/account-sync.ts +144 -0
- package/src/groups/messages.ts +89 -0
- package/src/groups/operations.ts +145 -0
- package/src/groups/repository.ts +161 -0
- package/src/groups/secrets.ts +256 -0
- package/src/groups/state.ts +138 -0
- package/src/identity/identifiers.ts +41 -0
- package/src/identity/neo.ts +72 -0
- package/src/index.ts +52 -0
- package/src/interactions.ts +35 -0
- package/src/messages/account-sender.ts +33 -0
- package/src/messages/contacts.ts +379 -0
- package/src/messages/outbox.ts +152 -0
- package/src/messages/receiver.ts +86 -0
- package/src/messages/repository.ts +230 -0
- package/src/models/authentication.ts +63 -0
- package/src/models/channels.ts +155 -0
- package/src/models/contacts.ts +141 -0
- package/src/models/content.ts +105 -0
- package/src/models/device-state.ts +18 -0
- package/src/models/group-admission.ts +110 -0
- package/src/models/group-keys.ts +34 -0
- package/src/models/group-management.ts +182 -0
- package/src/models/groups.ts +100 -0
- package/src/models/identity.ts +164 -0
- package/src/models/messages.ts +139 -0
- package/src/models/profile.ts +46 -0
- package/src/models/relay.ts +151 -0
- package/src/protocol/codec.ts +144 -0
- package/src/protocol/context.ts +39 -0
- package/src/protocol/encoding.ts +106 -0
- package/src/protocol/json.ts +140 -0
- package/src/protocol/validation.ts +13 -0
- package/src/runtime/abort.ts +27 -0
- package/src/runtime/async-gate.ts +16 -0
- package/src/runtime/async-pulse.ts +20 -0
- package/src/runtime/clock.ts +63 -0
- package/src/runtime/events.ts +31 -0
- package/src/runtime/url.ts +17 -0
- package/src/storage/signed-request.ts +26 -0
- package/src/storage/store.ts +90 -0
- package/src/storage/transaction.ts +17 -0
- package/src/transport/client.ts +328 -0
- package/src/transport/discovery.ts +64 -0
- package/src/transport/endpoint.ts +32 -0
- package/src/transport/http.ts +129 -0
- package/src/transport/pool.ts +115 -0
- package/src/transport/relay-error.ts +46 -0
- package/src/transport/rpc.ts +56 -0
- package/src/transport/session.ts +154 -0
- package/src/transport/socket.ts +248 -0
- package/src/transport/subscription.ts +117 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ProtocolError } from '../errors.js';
|
|
2
|
+
import type { DeviceSigner } from '../interactions.js';
|
|
3
|
+
import { type ContactAuthorization, type ContactConsent, type ContactGrant, type ContactInvite, type ContactRecord } from '../models/contacts.js';
|
|
4
|
+
import { type AccountDeviceState } from '../models/identity.js';
|
|
5
|
+
import { type MessageTimelineEntry } from '../models/messages.js';
|
|
6
|
+
import type { NetworkContext } from '../protocol/context.js';
|
|
7
|
+
import { type JsonObject } from '../protocol/json.js';
|
|
8
|
+
import type { RuntimeClock } from '../runtime/clock.js';
|
|
9
|
+
import type { MeshlineStore, QueryReader, RecordKey } from '../storage/store.js';
|
|
10
|
+
import { type MessageSendState, type OutboxEffects, type OutboxRecord } from './outbox.js';
|
|
11
|
+
import { type MessageEffects, type RecordGuard } from './repository.js';
|
|
12
|
+
export type ContactGrantState = 'unknown' | 'missing' | 'valid' | 'expired' | 'insufficientSignatures';
|
|
13
|
+
export interface ContactInfo {
|
|
14
|
+
readonly accountId: string;
|
|
15
|
+
readonly alias?: string;
|
|
16
|
+
readonly state: 'active';
|
|
17
|
+
readonly grantFromContact: ContactGrantState;
|
|
18
|
+
readonly grantToContact: ContactGrantState;
|
|
19
|
+
readonly updatedAt: number;
|
|
20
|
+
}
|
|
21
|
+
export type ContactRequestDirection = 'incoming' | 'outgoing';
|
|
22
|
+
export interface ContactRequestInfo {
|
|
23
|
+
readonly accountId: string;
|
|
24
|
+
readonly direction: ContactRequestDirection;
|
|
25
|
+
readonly createdAt: number;
|
|
26
|
+
readonly note?: string;
|
|
27
|
+
readonly messageId: string;
|
|
28
|
+
readonly sendState?: MessageSendState;
|
|
29
|
+
}
|
|
30
|
+
export type ContactChangeKind = 'relationship' | 'alias' | 'authorization' | 'deleted';
|
|
31
|
+
export interface ContactChange {
|
|
32
|
+
readonly accountId: string;
|
|
33
|
+
readonly contact?: ContactInfo;
|
|
34
|
+
readonly kinds: readonly ContactChangeKind[];
|
|
35
|
+
}
|
|
36
|
+
export type ContactRequestChangeKind = 'added' | 'updated' | 'removed';
|
|
37
|
+
export interface ContactRequestChange {
|
|
38
|
+
readonly accountId: string;
|
|
39
|
+
readonly direction: ContactRequestDirection;
|
|
40
|
+
readonly request?: ContactRequestInfo;
|
|
41
|
+
readonly kind: ContactRequestChangeKind;
|
|
42
|
+
}
|
|
43
|
+
export interface ContactChanges {
|
|
44
|
+
readonly contacts: readonly ContactChange[];
|
|
45
|
+
readonly requests: readonly ContactRequestChange[];
|
|
46
|
+
}
|
|
47
|
+
export interface StoredContact {
|
|
48
|
+
readonly record: ContactRecord;
|
|
49
|
+
readonly confirmedGrantTo?: ContactGrant;
|
|
50
|
+
readonly requiredDeviceRevision?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface StoredContactRequest extends ContactRequestInfo {
|
|
53
|
+
readonly consent: ContactConsent;
|
|
54
|
+
}
|
|
55
|
+
export interface PreparedContactSend {
|
|
56
|
+
readonly outbox: OutboxRecord;
|
|
57
|
+
readonly guards: readonly RecordGuard[];
|
|
58
|
+
}
|
|
59
|
+
export interface ContactHost {
|
|
60
|
+
readonly store: MeshlineStore;
|
|
61
|
+
readonly context: NetworkContext;
|
|
62
|
+
readonly accountId: string;
|
|
63
|
+
readonly device: DeviceSigner;
|
|
64
|
+
readonly clock: RuntimeClock;
|
|
65
|
+
getState(target: string | ContactAuthorization, signal?: AbortSignal): Promise<AccountDeviceState>;
|
|
66
|
+
prepareSend(recipient: string, payload: JsonObject, authorization: ContactAuthorization | undefined, recipientDevices: readonly string[] | undefined, signal?: AbortSignal): Promise<PreparedContactSend>;
|
|
67
|
+
changed(changes: ContactChanges): void;
|
|
68
|
+
}
|
|
69
|
+
export declare const contactKey: (account: string) => RecordKey;
|
|
70
|
+
export declare const contactRequestKey: (account: string, direction: ContactRequestDirection) => RecordKey;
|
|
71
|
+
export declare const cachedDeviceStateKey: (account: string) => RecordKey;
|
|
72
|
+
export declare function readContact(value: JsonObject | undefined): StoredContact | undefined;
|
|
73
|
+
export declare function encodeContact(value: StoredContact): JsonObject;
|
|
74
|
+
export declare function readContactRequest(value: JsonObject | undefined): StoredContactRequest | undefined;
|
|
75
|
+
export declare function encodeContactRequest(value: StoredContactRequest): JsonObject;
|
|
76
|
+
export declare function requestInfo(value: StoredContactRequest): ContactRequestInfo;
|
|
77
|
+
export declare class ContactMessageRejection extends ProtocolError {
|
|
78
|
+
}
|
|
79
|
+
/** Contact workflows prepare signatures/encrypted synchronization outside the eventual atomic transaction. */
|
|
80
|
+
export declare class MessageContacts {
|
|
81
|
+
#private;
|
|
82
|
+
readonly host: ContactHost;
|
|
83
|
+
constructor(host: ContactHost);
|
|
84
|
+
get(account: string, signal?: AbortSignal): Promise<ContactInfo | undefined>;
|
|
85
|
+
list(search?: string, signal?: AbortSignal): Promise<QueryReader<ContactInfo>>;
|
|
86
|
+
requests(account?: string, direction?: ContactRequestDirection, signal?: AbortSignal): Promise<QueryReader<ContactRequestInfo>>;
|
|
87
|
+
invite(expiresAt: number, signal?: AbortSignal): Promise<ContactInvite>;
|
|
88
|
+
add(target: string | ContactInvite, note?: string, signal?: AbortSignal): Promise<ContactRequestInfo>;
|
|
89
|
+
accept(account: string, signal?: AbortSignal): Promise<ContactInfo>;
|
|
90
|
+
dismiss(account: string, signal?: AbortSignal): Promise<void>;
|
|
91
|
+
edit(account: string, change: {
|
|
92
|
+
readonly alias: string | null;
|
|
93
|
+
}, signal?: AbortSignal): Promise<ContactInfo>;
|
|
94
|
+
edit(account: string, change: {
|
|
95
|
+
readonly deleted: true;
|
|
96
|
+
}, signal?: AbortSignal): Promise<void>;
|
|
97
|
+
/** Called only after envelope signature and decryption pass. External lookups remain outside rejection wrapping. */
|
|
98
|
+
prepareReception(entry: MessageTimelineEntry, payload: JsonObject, signal?: AbortSignal): Promise<MessageEffects | undefined>;
|
|
99
|
+
deliveryEffects(grants: ReadonlyMap<string, ContactGrant>): OutboxEffects;
|
|
100
|
+
/** Repairs local grant signatures and acknowledges peer device-state revisions with durable outgoing messages. */
|
|
101
|
+
maintain(account: string, notifyDeviceRevision: number | undefined, signal?: AbortSignal): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=contacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contacts.d.ts","sourceRoot":"","sources":["../../src/messages/contacts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { ProtocolError, StateConflictError } from '../errors.js';
|
|
2
|
+
import { validateIdentifier } from '../identity/identifiers.js';
|
|
3
|
+
import { validateAccountId } from '../identity/neo.js';
|
|
4
|
+
import { accountContactSyncCodec, contactConsentCodec, contactGrantCodec, contactGrantInput, contactInviteCodec, contactInviteInput, contactRecordCodec, filterContactGrantSignatures, selectContactGrant, validateAccountContactSync, validateContactConsent, validateContactInvite, validateContactRecord, verifyContactGrant } from '../models/contacts.js';
|
|
5
|
+
import { accountDeviceStateCodec, authorizedDevice, certificateId, validateDeviceState } from '../models/identity.js';
|
|
6
|
+
import { deviceStateChangedCodec, validateDeviceStateChanged } from '../models/messages.js';
|
|
7
|
+
import { encodeUtf8 } from '../protocol/encoding.js';
|
|
8
|
+
import { canonicalJson, requireSafeInteger } from '../protocol/json.js';
|
|
9
|
+
import { updateStore } from '../storage/transaction.js';
|
|
10
|
+
import { decodeOutbox, encodeOutbox, outboxKey } from './outbox.js';
|
|
11
|
+
import { snapshotReader } from './repository.js';
|
|
12
|
+
export const contactKey = (account) => ({ collection: 'contacts', key: account });
|
|
13
|
+
export const contactRequestKey = (account, direction) => ({ collection: 'contact_requests', key: `${account}|${direction}` });
|
|
14
|
+
export const cachedDeviceStateKey = (account) => ({ collection: 'device_states', key: account });
|
|
15
|
+
const fingerprint = (value) => canonicalJson(value ?? null);
|
|
16
|
+
const recordId = (key) => `${key.collection}|${key.key}`;
|
|
17
|
+
const put = (key, value) => ({ kind: 'put', ...key, value });
|
|
18
|
+
const removeRequests = (account) => ['incoming', 'outgoing'].map(direction => ({ kind: 'delete', ...contactRequestKey(account, direction) }));
|
|
19
|
+
const sends = new Set(['queued', 'submitting', 'submissionUnknown', 'relayAccepted', 'targetAccepted', 'failed', 'canceled']);
|
|
20
|
+
export function readContact(value) {
|
|
21
|
+
if (!value)
|
|
22
|
+
return undefined;
|
|
23
|
+
const record = contactRecordCodec.decode(value.record);
|
|
24
|
+
if (value.requiredDeviceRevision !== undefined)
|
|
25
|
+
requireSafeInteger(value.requiredDeviceRevision, 0);
|
|
26
|
+
return { record, ...(value.confirmedGrantTo === undefined ? {} : { confirmedGrantTo: contactGrantCodec.decode(value.confirmedGrantTo) }),
|
|
27
|
+
...(value.requiredDeviceRevision === undefined ? {} : { requiredDeviceRevision: value.requiredDeviceRevision }) };
|
|
28
|
+
}
|
|
29
|
+
export function encodeContact(value) {
|
|
30
|
+
return { record: contactRecordCodec.encode(value.record), ...(value.confirmedGrantTo ? { confirmedGrantTo: contactGrantCodec.encode(value.confirmedGrantTo) } : {}),
|
|
31
|
+
...(value.requiredDeviceRevision === undefined ? {} : { requiredDeviceRevision: value.requiredDeviceRevision }) };
|
|
32
|
+
}
|
|
33
|
+
export function readContactRequest(value) {
|
|
34
|
+
if (!value)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof value.accountId !== 'string' || (value.direction !== 'incoming' && value.direction !== 'outgoing') || typeof value.messageId !== 'string'
|
|
37
|
+
|| value.sendState !== undefined && (typeof value.sendState !== 'string' || !sends.has(value.sendState)))
|
|
38
|
+
throw new ProtocolError('invalid_storage', 'Invalid stored contact request.');
|
|
39
|
+
validateAccountId(value.accountId);
|
|
40
|
+
validateIdentifier('message', value.messageId);
|
|
41
|
+
requireSafeInteger(value.createdAt, 0);
|
|
42
|
+
const consent = contactConsentCodec.decode(value.consent);
|
|
43
|
+
return { accountId: value.accountId, direction: value.direction, createdAt: value.createdAt, messageId: value.messageId, consent,
|
|
44
|
+
...(consent.note === undefined ? {} : { note: consent.note }), ...(value.sendState === undefined ? {} : { sendState: value.sendState }) };
|
|
45
|
+
}
|
|
46
|
+
export function encodeContactRequest(value) {
|
|
47
|
+
return { accountId: value.accountId, direction: value.direction, createdAt: value.createdAt, messageId: value.messageId, consent: contactConsentCodec.encode(value.consent),
|
|
48
|
+
...(value.sendState === undefined ? {} : { sendState: value.sendState }) };
|
|
49
|
+
}
|
|
50
|
+
export function requestInfo(value) { const { consent: _consent, ...info } = value; return info; }
|
|
51
|
+
export class ContactMessageRejection extends ProtocolError {
|
|
52
|
+
}
|
|
53
|
+
function reject(code, message) { throw new ContactMessageRejection(code, message); }
|
|
54
|
+
function pureValidation(action) {
|
|
55
|
+
try {
|
|
56
|
+
return action();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof ProtocolError && !(error instanceof ContactMessageRejection))
|
|
60
|
+
throw new ContactMessageRejection(error.code, error.message, { cause: error });
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Contact workflows prepare signatures/encrypted synchronization outside the eventual atomic transaction. */
|
|
65
|
+
export class MessageContacts {
|
|
66
|
+
host;
|
|
67
|
+
constructor(host) {
|
|
68
|
+
this.host = host;
|
|
69
|
+
}
|
|
70
|
+
#checkAccount(account) { validateAccountId(account); if (account === this.host.accountId)
|
|
71
|
+
throw new ProtocolError('invalid_contact', 'An account cannot add itself as a contact.'); }
|
|
72
|
+
#nextTime(previous = -1) { const next = Math.max(this.host.clock.nowSeconds(), previous + 1); requireSafeInteger(next, 0); return next; }
|
|
73
|
+
async #snapshot(keys, signal) {
|
|
74
|
+
const snapshot = await this.host.store.read(keys, signal);
|
|
75
|
+
return keys.map((query, index) => ({ query, value: snapshot.sets[index][0]?.value }));
|
|
76
|
+
}
|
|
77
|
+
#effect(guards, mutations, prepared = [], kinds = ['relationship', 'authorization']) {
|
|
78
|
+
const all = [...guards, ...prepared.flatMap(value => [...value.guards, { query: outboxKey(value.outbox.request.envelope.messageId), value: undefined }])];
|
|
79
|
+
const expected = new Map();
|
|
80
|
+
for (const guard of all) {
|
|
81
|
+
const id = `${guard.query.collection}|${guard.query.key}`;
|
|
82
|
+
const previous = expected.get(id);
|
|
83
|
+
if (previous && fingerprint(previous.value) !== fingerprint(guard.value))
|
|
84
|
+
throw new StateConflictError('Contact preparation used inconsistent snapshots.');
|
|
85
|
+
expected.set(id, guard);
|
|
86
|
+
}
|
|
87
|
+
const unique = [...expected.values()];
|
|
88
|
+
const queries = new Map(unique.map(value => [recordId(value.query), value.query]));
|
|
89
|
+
for (const mutation of mutations) {
|
|
90
|
+
queries.set(recordId(mutation), { collection: mutation.collection, key: mutation.key });
|
|
91
|
+
if (mutation.collection === 'contacts') {
|
|
92
|
+
const key = cachedDeviceStateKey(mutation.key);
|
|
93
|
+
queries.set(recordId(key), key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const own = cachedDeviceStateKey(this.host.accountId);
|
|
97
|
+
queries.set(recordId(own), own);
|
|
98
|
+
return { queries: [...queries.values()], committed: (snapshot, changes) => this.host.changed(this.#changes(snapshot, changes, kinds)), plan: snapshot => {
|
|
99
|
+
for (let index = 0; index < unique.length; index++)
|
|
100
|
+
if (fingerprint(snapshot.sets[index][0]?.value) !== fingerprint(unique[index].value))
|
|
101
|
+
throw new StateConflictError('Contact state changed while preparing signed messages.');
|
|
102
|
+
return [...mutations, ...prepared.map(value => put(outboxKey(value.outbox.request.envelope.messageId), encodeOutbox(value.outbox)))];
|
|
103
|
+
} };
|
|
104
|
+
}
|
|
105
|
+
async #commit(effect, signal) {
|
|
106
|
+
const result = await updateStore(this.host.store, effect.queries, snapshot => { const mutations = effect.plan(snapshot); return { mutations, result: { snapshot, mutations } }; }, signal);
|
|
107
|
+
effect.committed?.(result.snapshot, result.mutations);
|
|
108
|
+
return result.snapshot;
|
|
109
|
+
}
|
|
110
|
+
#changes(snapshot, mutations, kinds) {
|
|
111
|
+
const before = new Map(snapshot.sets.flat().map(row => [recordId(row), row.value]));
|
|
112
|
+
const after = new Map(before);
|
|
113
|
+
for (const mutation of mutations) {
|
|
114
|
+
if (mutation.kind === 'put')
|
|
115
|
+
after.set(recordId(mutation), mutation.value);
|
|
116
|
+
else
|
|
117
|
+
after.delete(recordId(mutation));
|
|
118
|
+
}
|
|
119
|
+
const contacts = [];
|
|
120
|
+
const requests = [];
|
|
121
|
+
const visited = new Set();
|
|
122
|
+
for (const mutation of mutations) {
|
|
123
|
+
const id = recordId(mutation);
|
|
124
|
+
if (visited.has(id) || fingerprint(before.get(id)) === fingerprint(after.get(id)))
|
|
125
|
+
continue;
|
|
126
|
+
visited.add(id);
|
|
127
|
+
if (mutation.collection === 'contacts' && kinds.length) {
|
|
128
|
+
const contact = readContact(after.get(id));
|
|
129
|
+
contacts.push({ accountId: mutation.key, kinds: contact?.record.status === 'active' ? [...kinds] : ['deleted'],
|
|
130
|
+
...(contact?.record.status === 'active' ? { contact: this.#info(contact, state(after.get(recordId(cachedDeviceStateKey(this.host.accountId)))), state(after.get(recordId(cachedDeviceStateKey(mutation.key))))) } : {}) });
|
|
131
|
+
}
|
|
132
|
+
if (mutation.collection === 'contact_requests') {
|
|
133
|
+
const previous = readContactRequest(before.get(id));
|
|
134
|
+
const current = readContactRequest(after.get(id));
|
|
135
|
+
const request = current ?? previous;
|
|
136
|
+
if (request)
|
|
137
|
+
requests.push({ accountId: request.accountId, direction: request.direction,
|
|
138
|
+
kind: !current ? 'removed' : !previous || current.direction === 'outgoing' && current.messageId !== previous.messageId ? 'added' : 'updated',
|
|
139
|
+
...(current ? { request: requestInfo(current) } : {}) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { contacts, requests };
|
|
143
|
+
}
|
|
144
|
+
#protocolRecord(contact) {
|
|
145
|
+
const now = this.host.clock.nowSeconds();
|
|
146
|
+
const { grantFromContact, grantToContact, ...record } = contact.record;
|
|
147
|
+
return { ...record, ...(grantFromContact && (grantFromContact.expiresAt ?? Infinity) > now ? { grantFromContact } : {}),
|
|
148
|
+
...(grantToContact && (grantToContact.expiresAt ?? Infinity) > now ? { grantToContact } : {}) };
|
|
149
|
+
}
|
|
150
|
+
async #sync(contact, signal) {
|
|
151
|
+
return this.host.prepareSend(this.host.accountId, accountContactSyncCodec.encode({ records: [this.#protocolRecord(contact)] }), undefined, undefined, signal);
|
|
152
|
+
}
|
|
153
|
+
#grantState(grant, state) {
|
|
154
|
+
if (!grant)
|
|
155
|
+
return 'missing';
|
|
156
|
+
if ((grant.expiresAt ?? Infinity) <= this.host.clock.nowSeconds())
|
|
157
|
+
return 'expired';
|
|
158
|
+
if (!state)
|
|
159
|
+
return 'unknown';
|
|
160
|
+
return Object.keys(filterContactGrantSignatures(grant, state, this.host.context, this.host.clock.nowSeconds()).signatures).length ? 'valid' : 'insufficientSignatures';
|
|
161
|
+
}
|
|
162
|
+
#info(contact, own, remote) {
|
|
163
|
+
const record = contact.record;
|
|
164
|
+
return { accountId: record.account, ...(record.alias === undefined ? {} : { alias: record.alias }), state: 'active', updatedAt: record.updatedAt,
|
|
165
|
+
grantFromContact: this.#grantState(record.grantFromContact, remote), grantToContact: this.#grantState(record.grantToContact, own) };
|
|
166
|
+
}
|
|
167
|
+
async get(account, signal) {
|
|
168
|
+
this.#checkAccount(account);
|
|
169
|
+
const rows = await this.host.store.read([contactKey(account), cachedDeviceStateKey(this.host.accountId), cachedDeviceStateKey(account)], signal);
|
|
170
|
+
const contact = readContact(rows.sets[0][0]?.value);
|
|
171
|
+
if (!contact || contact.record.status !== 'active')
|
|
172
|
+
return undefined;
|
|
173
|
+
return this.#info(contact, state(rows.sets[1][0]?.value), state(rows.sets[2][0]?.value));
|
|
174
|
+
}
|
|
175
|
+
async list(search = '', signal) {
|
|
176
|
+
const rows = await this.host.store.read([{ collection: 'contacts' }, { collection: 'device_states' }], signal);
|
|
177
|
+
const states = new Map(rows.sets[1].map(row => [row.key, state(row.value)]));
|
|
178
|
+
return snapshotReader(rows.sets[0].map(row => readContact(row.value)).filter(value => value.record.status === 'active' && (!search || value.record.account.includes(search) || value.record.alias?.includes(search)))
|
|
179
|
+
.map(value => this.#info(value, states.get(this.host.accountId), states.get(value.record.account))));
|
|
180
|
+
}
|
|
181
|
+
async requests(account, direction, signal) {
|
|
182
|
+
if (account !== undefined)
|
|
183
|
+
this.#checkAccount(account);
|
|
184
|
+
if (direction !== undefined && direction !== 'incoming' && direction !== 'outgoing')
|
|
185
|
+
throw new TypeError('Unknown contact request direction.');
|
|
186
|
+
const rows = await this.host.store.read([{ collection: 'contact_requests' }], signal);
|
|
187
|
+
const values = rows.sets[0].map(row => readContactRequest(row.value)).filter(value => (account === undefined || value.accountId === account) && (direction === undefined || value.direction === direction))
|
|
188
|
+
.sort((a, b) => a.createdAt - b.createdAt || ordinal(a.accountId, b.accountId) || ordinal(a.direction, b.direction));
|
|
189
|
+
return snapshotReader(values.map(requestInfo));
|
|
190
|
+
}
|
|
191
|
+
async invite(expiresAt, signal) {
|
|
192
|
+
requireSafeInteger(expiresAt, 0);
|
|
193
|
+
const id = certificateId(this.host.device.certificate, this.host.context);
|
|
194
|
+
let invitation = { inviter: this.host.accountId, signerDeviceId: id, expiresAt, deviceSignature: new Uint8Array(64) };
|
|
195
|
+
validateContactInvite(invitation, this.host.clock.nowSeconds());
|
|
196
|
+
invitation = { ...invitation, deviceSignature: await this.host.device.sign(contactInviteInput(invitation, this.host.context), signal) };
|
|
197
|
+
if (certificateId(this.host.device.certificate, this.host.context) !== id)
|
|
198
|
+
throw new StateConflictError('Device identity changed while signing invitation.');
|
|
199
|
+
return invitation;
|
|
200
|
+
}
|
|
201
|
+
async #signGrant(grant, signal) {
|
|
202
|
+
const id = certificateId(this.host.device.certificate, this.host.context);
|
|
203
|
+
if (grant.signatures[id])
|
|
204
|
+
return grant;
|
|
205
|
+
const signature = await this.host.device.sign(contactGrantInput(grant, this.host.context), signal);
|
|
206
|
+
if (certificateId(this.host.device.certificate, this.host.context) !== id)
|
|
207
|
+
throw new StateConflictError('Device identity changed while signing contact grant.');
|
|
208
|
+
return { ...grant, signatures: { ...grant.signatures, [id]: signature } };
|
|
209
|
+
}
|
|
210
|
+
async #consent(account, note, signal) {
|
|
211
|
+
const own = await this.host.getState(this.host.accountId, signal);
|
|
212
|
+
authorizedDevice(own, certificateId(this.host.device.certificate, this.host.context), this.host.context, this.host.clock.nowSeconds());
|
|
213
|
+
const grant = await this.#signGrant({ grantor: this.host.accountId, grantee: account, signatures: {} }, signal);
|
|
214
|
+
const consent = { deviceState: own, grant, ...(note === undefined ? {} : { note }) };
|
|
215
|
+
validateContactConsent(consent, this.host.context, this.host.clock.nowSeconds());
|
|
216
|
+
return consent;
|
|
217
|
+
}
|
|
218
|
+
async add(target, note, signal) {
|
|
219
|
+
const invitation = typeof target === 'string' ? undefined : contactInviteCodec.decode(contactInviteCodec.encode(target));
|
|
220
|
+
const account = typeof target === 'string' ? target : target.inviter;
|
|
221
|
+
this.#checkAccount(account);
|
|
222
|
+
await this.host.getState(invitation ?? account, signal);
|
|
223
|
+
const consent = await this.#consent(account, note, signal);
|
|
224
|
+
const guards = await this.#snapshot([contactKey(account), contactRequestKey(account, 'outgoing')], signal);
|
|
225
|
+
if (readContact(guards[0].value)?.record.status === 'active')
|
|
226
|
+
throw new ProtocolError('contact_exists', 'The account is already a contact.');
|
|
227
|
+
const previous = readContactRequest(guards[1].value);
|
|
228
|
+
if (previous && previous.sendState !== 'failed' && previous.sendState !== 'canceled')
|
|
229
|
+
return requestInfo(previous);
|
|
230
|
+
const prepared = await this.host.prepareSend(account, contactConsentCodec.encode(consent), invitation, undefined, signal);
|
|
231
|
+
const envelope = prepared.outbox.request.envelope;
|
|
232
|
+
const request = { accountId: account, direction: 'outgoing', createdAt: envelope.createdAt, messageId: envelope.messageId, consent, sendState: 'queued', ...(note === undefined ? {} : { note }) };
|
|
233
|
+
await this.#commit(this.#effect(guards, [put(contactRequestKey(account, 'outgoing'), encodeContactRequest(request))], [prepared]), signal);
|
|
234
|
+
return requestInfo(request);
|
|
235
|
+
}
|
|
236
|
+
async accept(account, signal) {
|
|
237
|
+
this.#checkAccount(account);
|
|
238
|
+
const guards = await this.#snapshot([contactKey(account), contactRequestKey(account, 'incoming'), contactRequestKey(account, 'outgoing')], signal);
|
|
239
|
+
const incoming = readContactRequest(guards[1].value);
|
|
240
|
+
if (!incoming)
|
|
241
|
+
throw new ProtocolError('contact_request_required', 'There is no incoming request from this account.');
|
|
242
|
+
const remote = await this.host.getState(incoming.consent.grant, signal);
|
|
243
|
+
const grant = this.#verified(incoming.consent.grant, remote);
|
|
244
|
+
const consent = await this.#consent(account, undefined, signal);
|
|
245
|
+
const previous = readContact(guards[0].value);
|
|
246
|
+
const contact = { record: { account, status: 'active', updatedAt: this.#nextTime(previous?.record.updatedAt), ...(previous?.record.alias === undefined ? {} : { alias: previous.record.alias }), grantFromContact: grant, grantToContact: consent.grant } };
|
|
247
|
+
const outgoing = await this.host.prepareSend(account, contactConsentCodec.encode(consent), grant, undefined, signal);
|
|
248
|
+
const sync = await this.#sync(contact, signal);
|
|
249
|
+
await this.#commit(this.#effect(guards, [put(contactKey(account), encodeContact(contact)), ...removeRequests(account)], [outgoing, sync]), signal);
|
|
250
|
+
return this.#info(contact, consent.deviceState, remote);
|
|
251
|
+
}
|
|
252
|
+
async dismiss(account, signal) {
|
|
253
|
+
this.#checkAccount(account);
|
|
254
|
+
const key = contactRequestKey(account, 'incoming');
|
|
255
|
+
await this.#commit({ queries: [key], plan: snapshot => snapshot.sets[0].length ? [{ kind: 'delete', ...key }] : [],
|
|
256
|
+
committed: (snapshot, mutations) => this.host.changed(this.#changes(snapshot, mutations, [])) }, signal);
|
|
257
|
+
}
|
|
258
|
+
async edit(account, change, signal) {
|
|
259
|
+
this.#checkAccount(account);
|
|
260
|
+
const initial = await this.#snapshot([contactKey(account), contactRequestKey(account, 'incoming'), contactRequestKey(account, 'outgoing'), cachedDeviceStateKey(this.host.accountId), cachedDeviceStateKey(account)], signal);
|
|
261
|
+
const guards = initial.slice(0, 3);
|
|
262
|
+
const previous = readContact(guards[0].value);
|
|
263
|
+
if (!change.deleted && previous?.record.status !== 'active')
|
|
264
|
+
throw new ProtocolError('contact_required', 'Alias changes require an active contact.');
|
|
265
|
+
const { alias: oldAlias, grantFromContact, grantToContact, ...base } = previous?.record ?? { account, status: 'deleted', updatedAt: 0 };
|
|
266
|
+
const alias = change.alias === null ? undefined : change.alias ?? oldAlias;
|
|
267
|
+
if (!change.deleted && alias === oldAlias)
|
|
268
|
+
return this.#info(previous, state(initial[3].value), state(initial[4].value));
|
|
269
|
+
const record = { ...base, status: change.deleted ? 'deleted' : 'active', updatedAt: this.#nextTime(previous?.record.updatedAt), ...(alias === undefined ? {} : { alias }),
|
|
270
|
+
...(!change.deleted && grantFromContact ? { grantFromContact } : {}), ...(!change.deleted && grantToContact ? { grantToContact } : {}) };
|
|
271
|
+
validateContactRecord(record, this.host.clock.nowSeconds());
|
|
272
|
+
const contact = change.deleted ? { record } : { ...previous, record };
|
|
273
|
+
const sync = await this.#sync(contact, signal);
|
|
274
|
+
const committed = await this.#commit(this.#effect(guards, [put(contactKey(account), encodeContact(contact)), ...(change.deleted ? removeRequests(account) : [])], [sync], [change.deleted ? 'deleted' : 'alias']), signal);
|
|
275
|
+
if (!change.deleted) {
|
|
276
|
+
// Return from the successful transaction's snapshot. A fresh read
|
|
277
|
+
// could race another write or be canceled by a disposal observer.
|
|
278
|
+
const devices = new Map(committed.sets.flat().filter(row => row.collection === 'device_states').map(row => [row.key, state(row.value)]));
|
|
279
|
+
return this.#info(contact, devices.get(this.host.accountId), devices.get(account));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
#verified(grant, devices) { verifyContactGrant(grant, devices, this.host.context, this.host.clock.nowSeconds()); return filterContactGrantSignatures(grant, devices, this.host.context, this.host.clock.nowSeconds()); }
|
|
283
|
+
#merge(previous, incoming, devices) {
|
|
284
|
+
const result = selectContactGrant(previous, incoming, devices, this.host.context, this.host.clock.nowSeconds());
|
|
285
|
+
if (result.action === 'invalid')
|
|
286
|
+
throw result.error;
|
|
287
|
+
if (result.action === 'out_of_scope' || !result.grant)
|
|
288
|
+
throw new ProtocolError('invalid_authorization', 'Contact grants have inconsistent scope.');
|
|
289
|
+
return result.grant;
|
|
290
|
+
}
|
|
291
|
+
/** Called only after envelope signature and decryption pass. External lookups remain outside rejection wrapping. */
|
|
292
|
+
async prepareReception(entry, payload, signal) {
|
|
293
|
+
const envelope = entry.envelope;
|
|
294
|
+
const type = payload['$type'];
|
|
295
|
+
const self = envelope.from === envelope.to;
|
|
296
|
+
if (envelope.from === this.host.accountId && !self)
|
|
297
|
+
return undefined;
|
|
298
|
+
if (['meshline.account.group.state.request', 'meshline.account.group.state.sync', 'meshline.account.group.history_secret.sync'].includes(type)) {
|
|
299
|
+
if (!self || envelope.from !== this.host.accountId)
|
|
300
|
+
reject('invalid_binding', 'Account group synchronization requires self-delivery.');
|
|
301
|
+
await this.host.getState(this.host.accountId, signal);
|
|
302
|
+
return this.#authorize(entry);
|
|
303
|
+
}
|
|
304
|
+
if (type === 'meshline.contact.consent') {
|
|
305
|
+
if (self)
|
|
306
|
+
reject('invalid_binding', 'Contact consent cannot be self-delivered.');
|
|
307
|
+
const consent = pureValidation(() => { const value = contactConsentCodec.decode(payload); validateContactConsent(value, this.host.context, this.host.clock.nowSeconds()); return value; });
|
|
308
|
+
return this.#receiveConsent(entry, consent, signal);
|
|
309
|
+
}
|
|
310
|
+
const contacts = ['meshline.contact.grant', 'meshline.device.state.changed', 'meshline.account.contacts.sync'];
|
|
311
|
+
if (contacts.includes(type) && self !== (type === 'meshline.account.contacts.sync'))
|
|
312
|
+
reject('invalid_binding', 'Contact payload has invalid sender/recipient account binding.');
|
|
313
|
+
if (type === 'meshline.account.contacts.sync')
|
|
314
|
+
return this.#receiveSync(entry, pureValidation(() => { const value = accountContactSyncCodec.decode(payload); validateAccountContactSync(value, this.host.clock.nowSeconds(), this.host.accountId); return value; }), signal);
|
|
315
|
+
if (type !== 'meshline.message.direct' && type !== 'meshline.contact.grant' && type !== 'meshline.device.state.changed')
|
|
316
|
+
return undefined;
|
|
317
|
+
await this.host.getState(this.host.accountId, signal);
|
|
318
|
+
if (type === 'meshline.contact.grant') {
|
|
319
|
+
const grant = pureValidation(() => contactGrantCodec.decode(payload));
|
|
320
|
+
if (grant.grantor !== envelope.from || grant.grantee !== envelope.to)
|
|
321
|
+
reject('invalid_binding', 'Contact grant differs from envelope participants.');
|
|
322
|
+
const remote = await this.host.getState(grant, signal);
|
|
323
|
+
pureValidation(() => this.#verified(grant, remote));
|
|
324
|
+
const guards = await this.#snapshot([contactKey(envelope.from)], signal);
|
|
325
|
+
const current = readContact(guards[0].value);
|
|
326
|
+
if (!current || current.record.status !== 'active')
|
|
327
|
+
reject('contact_required', 'No active contact exists for this sender.');
|
|
328
|
+
const merged = pureValidation(() => this.#merge(current.record.grantFromContact, grant, remote));
|
|
329
|
+
if (current.record.grantFromContact && contactGrantCodec.stringify(merged) === contactGrantCodec.stringify(current.record.grantFromContact))
|
|
330
|
+
return this.#authorize(entry);
|
|
331
|
+
const next = { ...current, record: { ...current.record, grantFromContact: merged, updatedAt: this.#nextTime(current.record.updatedAt) } };
|
|
332
|
+
const sync = await this.#sync(next, signal);
|
|
333
|
+
return this.#authorize(entry, this.#effect(guards, [put(contactKey(envelope.from), encodeContact(next))], [sync], ['authorization']));
|
|
334
|
+
}
|
|
335
|
+
if (type === 'meshline.device.state.changed') {
|
|
336
|
+
const change = pureValidation(() => { const value = deviceStateChangedCodec.decode(payload); validateDeviceStateChanged(value); return value; });
|
|
337
|
+
const key = contactKey(envelope.from);
|
|
338
|
+
return this.#authorize(entry, { queries: [key, cachedDeviceStateKey(envelope.from)], plan: snapshot => {
|
|
339
|
+
const contact = readContact(snapshot.sets[0][0]?.value);
|
|
340
|
+
if (!contact || contact.record.status !== 'active')
|
|
341
|
+
reject('contact_required', 'Contact is no longer active.');
|
|
342
|
+
const remote = state(snapshot.sets[1][0]?.value);
|
|
343
|
+
if (remote && remote.revision >= change.revision)
|
|
344
|
+
return [];
|
|
345
|
+
return [put(key, encodeContact({ ...contact, requiredDeviceRevision: Math.max(change.revision, contact.requiredDeviceRevision ?? -1) }))];
|
|
346
|
+
} });
|
|
347
|
+
}
|
|
348
|
+
return this.#authorize(entry);
|
|
349
|
+
}
|
|
350
|
+
#authorize(entry, effect) {
|
|
351
|
+
const self = entry.envelope.from === entry.envelope.to;
|
|
352
|
+
const account = entry.envelope.from;
|
|
353
|
+
return { queries: [cachedDeviceStateKey(this.host.accountId), contactKey(account), ...effect?.queries ?? []],
|
|
354
|
+
...(effect?.committed ? { committed: (snapshot, mutations) => effect.committed({ ...snapshot, sets: snapshot.sets.slice(2) }, mutations) } : {}), plan: snapshot => {
|
|
355
|
+
const own = state(snapshot.sets[0][0]?.value);
|
|
356
|
+
if (!own)
|
|
357
|
+
throw new ProtocolError('device_state_required', 'Current own device state is unavailable.');
|
|
358
|
+
pureValidation(() => {
|
|
359
|
+
if (self)
|
|
360
|
+
authorizedDevice(own, entry.envelope.fromDeviceId, this.host.context, this.host.clock.nowSeconds());
|
|
361
|
+
else {
|
|
362
|
+
const contact = readContact(snapshot.sets[1][0]?.value);
|
|
363
|
+
if (contact?.record.status !== 'active' || !contact.record.grantToContact)
|
|
364
|
+
reject('contact_required', 'No active locally issued grant remains for this sender.');
|
|
365
|
+
verifyContactGrant(contact.record.grantToContact, own, this.host.context, this.host.clock.nowSeconds());
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return effect?.plan({ ...snapshot, sets: snapshot.sets.slice(2) }) ?? [];
|
|
369
|
+
} };
|
|
370
|
+
}
|
|
371
|
+
async #receiveConsent(entry, consent, signal) {
|
|
372
|
+
const account = entry.envelope.from;
|
|
373
|
+
pureValidation(() => {
|
|
374
|
+
if (consent.deviceState.account !== account || consent.grant.grantor !== account || consent.grant.grantee !== this.host.accountId)
|
|
375
|
+
reject('invalid_binding', 'Consent differs from envelope participants.');
|
|
376
|
+
authorizedDevice(consent.deviceState, entry.envelope.fromDeviceId, this.host.context, this.host.clock.nowSeconds());
|
|
377
|
+
if (!this.#verified(consent.grant, consent.deviceState).signatures[entry.envelope.fromDeviceId])
|
|
378
|
+
reject('invalid_signature', 'Consent sender did not sign its grant.');
|
|
379
|
+
});
|
|
380
|
+
const guards = [...await this.#snapshot([contactKey(account), contactRequestKey(account, 'incoming'), contactRequestKey(account, 'outgoing'), cachedDeviceStateKey(account)], signal)];
|
|
381
|
+
const previous = readContact(guards[0].value);
|
|
382
|
+
const waiting = readContactRequest(guards[2].value);
|
|
383
|
+
const cached = state(guards[3].value);
|
|
384
|
+
const waitingDelivery = waiting ? (await this.host.store.read([outboxKey(waiting.messageId)], signal)).sets[0][0] : undefined;
|
|
385
|
+
if (waiting)
|
|
386
|
+
guards.push({ query: outboxKey(waiting.messageId), value: waitingDelivery?.value });
|
|
387
|
+
const accepted = waiting?.sendState === 'targetAccepted' || waitingDelivery !== undefined && decodeOutbox(waitingDelivery).acceptedAt !== undefined;
|
|
388
|
+
if (cached && (cached.revision > consent.deviceState.revision || cached.revision === consent.deviceState.revision && accountDeviceStateCodec.stringify(cached) !== accountDeviceStateCodec.stringify(consent.deviceState)))
|
|
389
|
+
reject('stale_state', 'Consent contains an older or conflicting device state.');
|
|
390
|
+
const mutations = [put(cachedDeviceStateKey(account), accountDeviceStateCodec.encode(consent.deviceState))];
|
|
391
|
+
if (previous?.record.status === 'active' || waiting && waiting.sendState !== 'canceled') {
|
|
392
|
+
const grant = pureValidation(() => this.#merge(previous?.record.grantFromContact, consent.grant, consent.deviceState));
|
|
393
|
+
const outgoing = waiting?.consent.grant ?? previous?.record.grantToContact;
|
|
394
|
+
const record = { ...(previous?.record ?? { account, status: 'active', updatedAt: 0 }), status: 'active', updatedAt: this.#nextTime(previous?.record.updatedAt), grantFromContact: grant, ...(outgoing ? { grantToContact: outgoing } : {}) };
|
|
395
|
+
const current = { ...previous, record, ...(waiting && accepted && outgoing ? { confirmedGrantTo: outgoing } : {}) };
|
|
396
|
+
const sync = await this.#sync(current, signal);
|
|
397
|
+
mutations.push(put(contactKey(account), encodeContact(current)), ...removeRequests(account));
|
|
398
|
+
return this.#effect(guards, mutations, [sync]);
|
|
399
|
+
}
|
|
400
|
+
const request = { accountId: account, direction: 'incoming', messageId: entry.envelope.messageId, createdAt: entry.envelope.createdAt, consent, ...(consent.note === undefined ? {} : { note: consent.note }) };
|
|
401
|
+
mutations.push(put(contactRequestKey(account, 'incoming'), encodeContactRequest(request)));
|
|
402
|
+
return this.#effect(guards, mutations);
|
|
403
|
+
}
|
|
404
|
+
async #receiveSync(entry, sync, signal) {
|
|
405
|
+
const own = await this.host.getState(this.host.accountId, signal);
|
|
406
|
+
pureValidation(() => authorizedDevice(own, entry.envelope.fromDeviceId, this.host.context, this.host.clock.nowSeconds()));
|
|
407
|
+
const guards = await this.#snapshot((sync.records ?? []).map(value => contactKey(value.account)), signal);
|
|
408
|
+
const mutations = [];
|
|
409
|
+
const prepared = [];
|
|
410
|
+
for (let index = 0; index < (sync.records?.length ?? 0); index++) {
|
|
411
|
+
const source = sync.records[index];
|
|
412
|
+
const previous = readContact(guards[index].value);
|
|
413
|
+
if (previous && previous.record.updatedAt > source.updatedAt)
|
|
414
|
+
continue;
|
|
415
|
+
let incoming;
|
|
416
|
+
let outgoing;
|
|
417
|
+
let added = false;
|
|
418
|
+
if (source.grantFromContact) {
|
|
419
|
+
const remote = await this.host.getState(source.grantFromContact, signal);
|
|
420
|
+
incoming = pureValidation(() => this.#merge(previous?.record.grantFromContact, source.grantFromContact, remote));
|
|
421
|
+
}
|
|
422
|
+
if (source.grantToContact) {
|
|
423
|
+
const verified = pureValidation(() => this.#verified(source.grantToContact, own));
|
|
424
|
+
const supplemented = await this.#signGrant(verified, signal);
|
|
425
|
+
outgoing = pureValidation(() => this.#merge(previous?.record.grantToContact, supplemented, own));
|
|
426
|
+
added = contactGrantCodec.stringify(supplemented) !== contactGrantCodec.stringify(verified);
|
|
427
|
+
}
|
|
428
|
+
const current = source.status === 'deleted' ? { record: source } : { ...previous, record: { ...source,
|
|
429
|
+
...(incoming ?? previous?.record.grantFromContact ? { grantFromContact: incoming ?? previous.record.grantFromContact } : {}),
|
|
430
|
+
...(outgoing ?? previous?.record.grantToContact ? { grantToContact: outgoing ?? previous.record.grantToContact } : {}), ...(added ? { updatedAt: this.#nextTime(source.updatedAt) } : {}) } };
|
|
431
|
+
mutations.push(put(contactKey(source.account), encodeContact(current)));
|
|
432
|
+
if (source.status === 'deleted')
|
|
433
|
+
mutations.push(...removeRequests(source.account));
|
|
434
|
+
if (added) {
|
|
435
|
+
prepared.push(await this.#sync(current, signal));
|
|
436
|
+
if (current.record.grantFromContact && outgoing)
|
|
437
|
+
prepared.push(await this.host.prepareSend(source.account, contactGrantCodec.encode(outgoing), current.record.grantFromContact, undefined, signal));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (sync.requestSnapshot) {
|
|
441
|
+
const rows = await this.host.store.read([{ collection: 'contacts' }], signal);
|
|
442
|
+
const records = new Map(rows.sets[0].map(row => [row.key, readContact(row.value)]));
|
|
443
|
+
for (const mutation of mutations)
|
|
444
|
+
if (mutation.kind === 'put' && mutation.collection === 'contacts')
|
|
445
|
+
records.set(mutation.key, readContact(mutation.value));
|
|
446
|
+
let batch = [];
|
|
447
|
+
let size = 0;
|
|
448
|
+
const flush = async () => { if (!batch.length)
|
|
449
|
+
return; prepared.push(await this.host.prepareSend(this.host.accountId, accountContactSyncCodec.encode({ records: batch }), undefined, [entry.envelope.fromDeviceId], signal)); batch = []; size = 0; };
|
|
450
|
+
for (const record of records.values()) {
|
|
451
|
+
const item = this.#protocolRecord(record);
|
|
452
|
+
const bytes = encodeUtf8(contactRecordCodec.stringify(item)).length;
|
|
453
|
+
if (bytes > 131072)
|
|
454
|
+
throw new ProtocolError('invalid_size', 'A contact record exceeds synchronization limits.');
|
|
455
|
+
if (batch.length >= 128 || batch.length && size + bytes > 131072)
|
|
456
|
+
await flush();
|
|
457
|
+
batch.push(item);
|
|
458
|
+
size += bytes;
|
|
459
|
+
}
|
|
460
|
+
await flush();
|
|
461
|
+
}
|
|
462
|
+
return this.#authorize(entry, this.#effect(guards, mutations, prepared, ['relationship', 'alias', 'authorization']));
|
|
463
|
+
}
|
|
464
|
+
deliveryEffects(grants) {
|
|
465
|
+
return { queries: [{ collection: 'contact_requests' }, { collection: 'contacts' }, cachedDeviceStateKey(this.host.accountId)],
|
|
466
|
+
committed: (snapshot, mutations) => { if (mutations.length)
|
|
467
|
+
this.host.changed(this.#changes(snapshot, mutations, [])); }, plan: (snapshot, change) => {
|
|
468
|
+
const record = change.current;
|
|
469
|
+
const envelope = record.request.envelope;
|
|
470
|
+
if (record.isDirect || envelope.to === this.host.accountId)
|
|
471
|
+
return [];
|
|
472
|
+
const mutations = [];
|
|
473
|
+
const key = contactRequestKey(envelope.to, 'outgoing');
|
|
474
|
+
const requestRow = snapshot.sets[1].find(row => row.key === key.key);
|
|
475
|
+
const request = readContactRequest(requestRow?.value);
|
|
476
|
+
if (request?.messageId === envelope.messageId)
|
|
477
|
+
mutations.push(record.state === 'canceled' ? { kind: 'delete', ...key } : put(key, encodeContactRequest({ ...request, sendState: record.state })));
|
|
478
|
+
const contact = readContact(snapshot.sets[2].find(row => row.key === envelope.to)?.value);
|
|
479
|
+
const own = state(snapshot.sets[3][0]?.value);
|
|
480
|
+
const grant = grants.get(envelope.messageId);
|
|
481
|
+
if (record.acceptedAt !== undefined && contact?.record.status === 'active' && own && grant)
|
|
482
|
+
mutations.push(put(contactKey(envelope.to), encodeContact({ ...contact, confirmedGrantTo: this.#merge(contact.confirmedGrantTo, grant, own) })));
|
|
483
|
+
return mutations;
|
|
484
|
+
} };
|
|
485
|
+
}
|
|
486
|
+
/** Repairs local grant signatures and acknowledges peer device-state revisions with durable outgoing messages. */
|
|
487
|
+
async maintain(account, notifyDeviceRevision, signal) {
|
|
488
|
+
const guards = await this.#snapshot([contactKey(account)], signal);
|
|
489
|
+
const saved = readContact(guards[0].value);
|
|
490
|
+
if (!saved || saved.record.status !== 'active')
|
|
491
|
+
return;
|
|
492
|
+
const now = this.host.clock.nowSeconds();
|
|
493
|
+
const own = await this.host.getState(this.host.accountId, signal);
|
|
494
|
+
authorizedDevice(own, certificateId(this.host.device.certificate, this.host.context), this.host.context, now);
|
|
495
|
+
const incoming = saved.record.grantFromContact && (saved.record.grantFromContact.expiresAt ?? Infinity) > now ? saved.record.grantFromContact : undefined;
|
|
496
|
+
let revisionResolved = false;
|
|
497
|
+
if (saved.requiredDeviceRevision !== undefined && incoming) {
|
|
498
|
+
const remote = await this.host.getState(incoming, signal);
|
|
499
|
+
if (remote.revision < saved.requiredDeviceRevision)
|
|
500
|
+
throw new ProtocolError('stale_device_state', 'Contact device state has not reached its announced revision.');
|
|
501
|
+
revisionResolved = true;
|
|
502
|
+
}
|
|
503
|
+
const issued = saved.record.grantToContact;
|
|
504
|
+
let supplemented;
|
|
505
|
+
if (issued && (issued.expiresAt ?? Infinity) > now)
|
|
506
|
+
supplemented = await this.#signGrant(filterContactGrantSignatures(issued, own, this.host.context, now), signal);
|
|
507
|
+
const changedGrant = supplemented !== undefined && contactGrantCodec.stringify(supplemented) !== contactGrantCodec.stringify(issued);
|
|
508
|
+
const { requiredDeviceRevision, ...withoutRevision } = saved;
|
|
509
|
+
const current = { ...(revisionResolved ? withoutRevision : saved), record: { ...saved.record, ...(changedGrant ? { grantToContact: supplemented, updatedAt: this.#nextTime(saved.record.updatedAt) } : {}) } };
|
|
510
|
+
const prepared = [];
|
|
511
|
+
if (changedGrant) {
|
|
512
|
+
prepared.push(await this.#sync(current, signal));
|
|
513
|
+
if (incoming)
|
|
514
|
+
prepared.push(await this.host.prepareSend(account, contactGrantCodec.encode(supplemented), incoming, undefined, signal));
|
|
515
|
+
}
|
|
516
|
+
if (notifyDeviceRevision !== undefined && incoming)
|
|
517
|
+
prepared.push(await this.host.prepareSend(account, deviceStateChangedCodec.encode({ revision: own.revision }), incoming, undefined, signal));
|
|
518
|
+
const mutations = changedGrant || revisionResolved ? [put(contactKey(account), encodeContact(current))] : [];
|
|
519
|
+
if (mutations.length || prepared.length)
|
|
520
|
+
await this.#commit(this.#effect(guards, mutations, prepared, ['authorization']), signal);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function state(value) { return value && accountDeviceStateCodec.decode(value); }
|
|
524
|
+
function ordinal(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
|
|
525
|
+
//# sourceMappingURL=contacts.js.map
|