@holochain/client 0.19.0-dev.0 → 0.19.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,8 +179,9 @@ You need `holochain` and `hc` on your path, best to get them from nix with `nix-
179
179
 
180
180
  To perform the pre-requisite DNA compilation steps, and run the Nodejs test, run:
181
181
  ```bash
182
- nix-shell
183
- ./run-test.sh
182
+ nix develop
183
+ ./build-fixture.sh
184
+ npm run test
184
185
  ```
185
186
 
186
187
  ## Contribute
@@ -120,6 +120,7 @@ export type AppInfo = {
120
120
  installed_app_id: InstalledAppId;
121
121
  cell_info: Record<RoleName, Array<CellInfo>>;
122
122
  status: InstalledAppInfoStatus;
123
+ installed_at: Timestamp;
123
124
  };
124
125
  /**
125
126
  * @public
@@ -1,5 +1,5 @@
1
1
  import { UnsubscribeFunction } from "emittery";
2
- import { AgentPubKey, AppAuthenticationToken, AppInfo, CapSecret, CellId, ClonedCell, DnaHash, DnaProperties, EntryHash, FunctionName, InstalledAppId, MembraneProof, MemproofMap, NetworkInfo, NetworkSeed, Nonce256Bit, RoleName, Timestamp, Transformer, WebsocketConnectionOptions, ZomeName } from "../../index.js";
2
+ import { AgentPubKey, AppAuthenticationToken, AppInfo, CapSecret, CellId, ClonedCell, DnaHash, DnaProperties, EntryHash, FunctionName, InstalledAppId, MembraneProof, MemproofMap, NetworkInfo, NetworkSeed, Nonce256Bit, RoleName, Timestamp, Transformer, WebsocketConnectionOptions, ZomeName, PreflightRequest, SignedAction, SignedActionHashed } from "../../index.js";
3
3
  /**
4
4
  * @public
5
5
  */
@@ -183,6 +183,211 @@ export interface NetworkInfoRequest {
183
183
  */
184
184
  last_time_queried?: number;
185
185
  }
186
+ /**
187
+ * Cell id for which the countersigning session state is requested.
188
+ *
189
+ * @public
190
+ */
191
+ export type GetCountersigningSessionStateRequest = CellId;
192
+ /**
193
+ * @public
194
+ */
195
+ export type GetCountersigningSessionStateResponse = null | CountersigningSessionState;
196
+ /**
197
+ * Cell id for which the countersigning session should be abandoned.
198
+ *
199
+ * @public
200
+ */
201
+ export type AbandonCountersigningSessionStateRequest = CellId;
202
+ /**
203
+ * @public
204
+ */
205
+ export type AbandonCountersigningSessionStateResponse = null;
206
+ /**
207
+ * Cell id for which the countersigning session should be published.
208
+ *
209
+ * @public
210
+ */
211
+ export type PublishCountersigningSessionStateRequest = CellId;
212
+ /**
213
+ * @public
214
+ */
215
+ export type PublishCountersigningSessionStateResponse = null;
216
+ /**
217
+ * @public
218
+ */
219
+ export declare enum CountersigningSessionStateType {
220
+ Accepted = "Accepted",
221
+ SignaturesCollected = "SignaturesCollected",
222
+ Unknown = "Unknown"
223
+ }
224
+ /**
225
+ * @public
226
+ */
227
+ export type CountersigningSessionState =
228
+ /**
229
+ * This is the entry state. Accepting a countersigning session through the HDK will immediately
230
+ * register the countersigning session in this state, for management by the countersigning workflow.
231
+ *
232
+ * The session will stay in this state even when the agent commits their countersigning entry and only
233
+ * move to the next state when the first signature bundle is received.
234
+ */
235
+ {
236
+ [CountersigningSessionStateType.Accepted]: PreflightRequest;
237
+ }
238
+ /**
239
+ * This is the state where we have collected one or more signatures for a countersigning session.
240
+ *
241
+ * This state can be entered from the [CountersigningSessionState::Accepted] state, which happens
242
+ * when a witness returns a signature bundle to us. While the session has not timed out, we will
243
+ * stay in this state and wait until one of the signatures bundles we have received is valid for
244
+ * the session to be completed.
245
+ *
246
+ * If we entered this state from the [CountersigningSessionState::Accepted] state, we will either
247
+ * complete the session successfully or the session will time out. On a timeout we will move
248
+ * to the [CountersigningSessionState::Unknown] for a limited number of attempts to recover the session.
249
+ *
250
+ * This state can also be entered from the [CountersigningSessionState::Unknown] state, which happens when we
251
+ * have been able to recover the session from the source chain and have requested signed actions
252
+ * from agent authorities to build a signature bundle.
253
+ *
254
+ * If we entered this state from the [CountersigningSessionState::Unknown] state, we will either
255
+ * complete the session successfully, or if the signatures are invalid, we will return to the
256
+ * [CountersigningSessionState::Unknown] state.
257
+ */
258
+ | {
259
+ [CountersigningSessionStateType.SignaturesCollected]: {
260
+ /** The preflight request that has been exchanged among countersigning peers. */
261
+ preflight_request: PreflightRequest;
262
+ /** Signed actions of the committed countersigned entries of all participating peers. */
263
+ signature_bundles: SignedAction[][];
264
+ /**
265
+ * This field is set when the signature bundle came from querying agent activity authorities
266
+ * in the unknown state. If we started from that state, we should return to it if the
267
+ * signature bundle is invalid. Otherwise, stay in this state and wait for more signatures.
268
+ */
269
+ resolution?: SessionResolutionSummary;
270
+ };
271
+ }
272
+ /**
273
+ * The session is in an unknown state and needs to be resolved.
274
+ *
275
+ * This state is used when we have lost track of the countersigning session. This happens if
276
+ * we have got far enough to create the countersigning entry but have crashed or restarted
277
+ * before we could complete the session. In this case we need to try to discover what the other
278
+ * agent or agents involved in the session have done.
279
+ *
280
+ * This state is also entered temporarily when we have published a signature and then the
281
+ * session has timed out. To avoid deadlocking with two parties both waiting for each other to
282
+ * proceed, we cannot stay in this state indefinitely. We will make a limited number of attempts
283
+ * to recover and if we cannot, we will abandon the session.
284
+ *
285
+ * The only exception to the attempt limiting is if we are unable to reach agent activity authorities
286
+ * to progress resolving the session. In this case, the attempts are not counted towards the
287
+ * configured limit. This does not protect us against a network partition where we can only see
288
+ * a subset of the network, but it does protect us against Holochain forcing a decision while
289
+ * it is unable to reach any peers.
290
+ *
291
+ * Note that because the [PreflightRequest] is stored here, we only ever enter the unknown state
292
+ * if we managed to keep the preflight request in memory, or if we have been able to recover it
293
+ * from the source chain as part of the committed [CounterSigningSessionData]. Otherwise, we
294
+ * are unable to discover what session we were participating in, and we must abandon the session
295
+ * without going through this recovery state.
296
+ */
297
+ | {
298
+ [CountersigningSessionStateType.Unknown]: {
299
+ /** The preflight request that has been exchanged. */
300
+ preflight_request: PreflightRequest;
301
+ /** Summary of the attempts to resolve this session. */
302
+ resolution: SessionResolutionSummary;
303
+ /** Flag if the session is programmed to be force-abandoned on the next countersigning workflow run. */
304
+ force_abandon: boolean;
305
+ /** Flag if the session is programmed to be force-published on the next countersigning workflow run. */
306
+ force_publish: boolean;
307
+ };
308
+ };
309
+ /**
310
+ * Summary of the workflow's attempts to resolve the outcome a failed countersigning session.
311
+ * This tracks the numbers of attempts and the outcome of the most recent attempt.
312
+ *
313
+ * @public
314
+ */
315
+ export interface SessionResolutionSummary {
316
+ /** The reason why session resolution is required. */
317
+ required_reason: ResolutionRequiredReason;
318
+ /**
319
+ * How many attempts have been made to resolve the session.
320
+ *
321
+ * Attempts are made according to the frequency specified by [RETRY_UNKNOWN_SESSION_STATE_DELAY].
322
+ *
323
+ * This count is only correct for the current run of the Holochain conductor. If the conductor
324
+ * is restarted then this counter is also reset.
325
+ */
326
+ attempts: number;
327
+ /** The time of the last attempt to resolve the session. */
328
+ last_attempt_at?: Timestamp;
329
+ /** The outcome of the most recent attempt to resolve the session. */
330
+ outcomes: SessionResolutionOutcome[];
331
+ }
332
+ /**
333
+ * The reason why a countersigning session can not be resolved automatically and requires manual resolution.
334
+ *
335
+ * @public
336
+ */
337
+ export declare enum ResolutionRequiredReason {
338
+ /** The session has timed out, so we should try to resolve its state before abandoning. */
339
+ Timeout = "Timeout",
340
+ /** Something happened, like a conductor restart, and we lost track of the session. */
341
+ Unknown = "Unknown"
342
+ }
343
+ /**
344
+ * The outcome for a single agent who participated in a countersigning session.
345
+ *
346
+ * [NUM_AUTHORITIES_TO_QUERY] authorities are made to agent activity authorities for each agent,
347
+ * and the decisions are collected into [SessionResolutionOutcome::decisions].
348
+ *
349
+ * @public
350
+ */
351
+ export interface SessionResolutionOutcome {
352
+ /**
353
+ * The agent who participated in the countersigning session and is the subject of this
354
+ * resolution outcome.
355
+ */
356
+ agent: AgentPubKey;
357
+ /** The resolved decision for each authority for the subject agent. */
358
+ decisions: SessionCompletionDecision[];
359
+ }
360
+ /**
361
+ * Decision about an incomplete countersigning session.
362
+ *
363
+ * @public
364
+ */
365
+ export declare enum SessionCompletionDecisionType {
366
+ /** Evidence found on the network that this session completed successfully. */
367
+ Complete = "Complete",
368
+ /**
369
+ * Evidence found on the network that this session was abandoned and other agents have
370
+ * added to their chain without completing the session.
371
+ */
372
+ Abandoned = "Abandoned",
373
+ /**
374
+ * No evidence, or inconclusive evidence, was found on the network. Holochain will not make an
375
+ * automatic decision until the evidence is conclusive.
376
+ */
377
+ Indeterminate = "Indeterminate",
378
+ /**There were errors encountered while trying to resolve the session. Errors such as network
379
+ * errors are treated differently to inconclusive evidence. We don't want to force a decision
380
+ * when we're offline, for example. In this case, the resolution must be retried later and this
381
+ * attempt should not be counted.
382
+ */
383
+ Failed = "Failed"
384
+ }
385
+ /**
386
+ * @public
387
+ */
388
+ export type SessionCompletionDecision = {
389
+ [SessionCompletionDecisionType.Complete]: SignedActionHashed;
390
+ } | SessionCompletionDecisionType.Abandoned | SessionCompletionDecisionType.Indeterminate | SessionCompletionDecisionType.Failed;
186
391
  /**
187
392
  * @public
188
393
  */
@@ -1,3 +1,50 @@
1
+ /**
2
+ * @public
3
+ */
4
+ export var CountersigningSessionStateType;
5
+ (function (CountersigningSessionStateType) {
6
+ CountersigningSessionStateType["Accepted"] = "Accepted";
7
+ CountersigningSessionStateType["SignaturesCollected"] = "SignaturesCollected";
8
+ CountersigningSessionStateType["Unknown"] = "Unknown";
9
+ })(CountersigningSessionStateType || (CountersigningSessionStateType = {}));
10
+ /**
11
+ * The reason why a countersigning session can not be resolved automatically and requires manual resolution.
12
+ *
13
+ * @public
14
+ */
15
+ export var ResolutionRequiredReason;
16
+ (function (ResolutionRequiredReason) {
17
+ /** The session has timed out, so we should try to resolve its state before abandoning. */
18
+ ResolutionRequiredReason["Timeout"] = "Timeout";
19
+ /** Something happened, like a conductor restart, and we lost track of the session. */
20
+ ResolutionRequiredReason["Unknown"] = "Unknown";
21
+ })(ResolutionRequiredReason || (ResolutionRequiredReason = {}));
22
+ /**
23
+ * Decision about an incomplete countersigning session.
24
+ *
25
+ * @public
26
+ */
27
+ export var SessionCompletionDecisionType;
28
+ (function (SessionCompletionDecisionType) {
29
+ /** Evidence found on the network that this session completed successfully. */
30
+ SessionCompletionDecisionType["Complete"] = "Complete";
31
+ /**
32
+ * Evidence found on the network that this session was abandoned and other agents have
33
+ * added to their chain without completing the session.
34
+ */
35
+ SessionCompletionDecisionType["Abandoned"] = "Abandoned";
36
+ /**
37
+ * No evidence, or inconclusive evidence, was found on the network. Holochain will not make an
38
+ * automatic decision until the evidence is conclusive.
39
+ */
40
+ SessionCompletionDecisionType["Indeterminate"] = "Indeterminate";
41
+ /**There were errors encountered while trying to resolve the session. Errors such as network
42
+ * errors are treated differently to inconclusive evidence. We don't want to force a decision
43
+ * when we're offline, for example. In this case, the resolution must be retried later and this
44
+ * attempt should not be counted.
45
+ */
46
+ SessionCompletionDecisionType["Failed"] = "Failed";
47
+ })(SessionCompletionDecisionType || (SessionCompletionDecisionType = {}));
1
48
  /**
2
49
  * @public
3
50
  */
@@ -1,7 +1,7 @@
1
1
  import { UnsubscribeFunction } from "emittery";
2
2
  import { AgentPubKey, CellId, InstalledAppId, RoleName } from "../../types.js";
3
3
  import { AppInfo, MemproofMap } from "../admin/index.js";
4
- import { AppCallZomeRequest, AppClient, AppEvents, AppNetworkInfoRequest, AppCreateCloneCellRequest, AppDisableCloneCellRequest, AppEnableCloneCellRequest, SignalCb, CallZomeRequest, CallZomeRequestSigned, CallZomeResponse, CreateCloneCellResponse, DisableCloneCellResponse, EnableCloneCellResponse, NetworkInfoResponse, AppWebsocketConnectionOptions } from "./types.js";
4
+ import { AppCallZomeRequest, AppClient, AppEvents, AppNetworkInfoRequest, AppCreateCloneCellRequest, AppDisableCloneCellRequest, AppEnableCloneCellRequest, SignalCb, CallZomeRequest, CallZomeRequestSigned, NetworkInfoResponse, AppWebsocketConnectionOptions, GetCountersigningSessionStateRequest, GetCountersigningSessionStateResponse, AbandonCountersigningSessionStateRequest, PublishCountersigningSessionStateRequest } from "./types.js";
5
5
  import { WsClient } from "../client.js";
6
6
  /**
7
7
  * A class to establish a websocket connection to an App interface, for a
@@ -25,6 +25,9 @@ export declare class AppWebsocket implements AppClient {
25
25
  private readonly enableCloneCellRequester;
26
26
  private readonly disableCloneCellRequester;
27
27
  private readonly networkInfoRequester;
28
+ private readonly getCountersigningSessionStateRequester;
29
+ private readonly abandonCountersigningSessionRequester;
30
+ private readonly publishCountersigningSessionRequester;
28
31
  private constructor();
29
32
  /**
30
33
  * Instance factory for creating an {@link AppWebsocket}.
@@ -67,33 +70,111 @@ export declare class AppWebsocket implements AppClient {
67
70
  * @param timeout - A timeout to override the default.
68
71
  * @returns The zome call's response.
69
72
  */
70
- callZome(request: AppCallZomeRequest, timeout?: number): Promise<CallZomeResponse>;
73
+ callZome<ReturnType>(request: AppCallZomeRequest, timeout?: number): Promise<ReturnType>;
71
74
  /**
72
75
  * Clone an existing provisioned cell.
73
76
  *
74
77
  * @param args - Specify the cell to clone.
75
78
  * @returns The created clone cell.
76
79
  */
77
- createCloneCell(args: AppCreateCloneCellRequest): Promise<CreateCloneCellResponse>;
80
+ createCloneCell(args: AppCreateCloneCellRequest): Promise<import("../admin/types.js").ClonedCell>;
78
81
  /**
79
82
  * Enable a disabled clone cell.
80
83
  *
81
84
  * @param args - Specify the clone cell to enable.
82
85
  * @returns The enabled clone cell.
83
86
  */
84
- enableCloneCell(args: AppEnableCloneCellRequest): Promise<EnableCloneCellResponse>;
87
+ enableCloneCell(args: AppEnableCloneCellRequest): Promise<import("../admin/types.js").ClonedCell>;
85
88
  /**
86
89
  * Disable an enabled clone cell.
87
90
  *
88
91
  * @param args - Specify the clone cell to disable.
89
92
  */
90
- disableCloneCell(args: AppDisableCloneCellRequest): Promise<DisableCloneCellResponse>;
93
+ disableCloneCell(args: AppDisableCloneCellRequest): Promise<void>;
91
94
  /**
92
95
  * Request network info about gossip status.
93
96
  * @param args - Specify the DNAs for which you want network info
94
97
  * @returns Network info for the specified DNAs
95
98
  */
96
99
  networkInfo(args: AppNetworkInfoRequest): Promise<NetworkInfoResponse>;
100
+ /**
101
+ * Get the state of a countersigning session.
102
+ */
103
+ getCountersigningSessionState(args: GetCountersigningSessionStateRequest): Promise<GetCountersigningSessionStateResponse>;
104
+ /**
105
+ * Abandon an unresolved countersigning session.
106
+ *
107
+ * If the current session has not been resolved automatically, it can be forcefully abandoned.
108
+ * A condition for this call to succeed is that at least one attempt has been made to resolve
109
+ * it automatically.
110
+ *
111
+ * # Returns
112
+ *
113
+ * [`AppResponse::CountersigningSessionAbandoned`]
114
+ *
115
+ * The session is marked for abandoning and the countersigning workflow was triggered. The session
116
+ * has not been abandoned yet.
117
+ *
118
+ * Upon successful abandoning the system signal [`SystemSignal::AbandonedCountersigning`] will
119
+ * be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
120
+ * would return `None`.
121
+ *
122
+ * In the countersigning workflow it will first be attempted to resolve the session with incoming
123
+ * signatures of the countersigned entries, before force-abandoning the session. In a very rare event
124
+ * it could happen that in just the moment where the [`AppRequest::AbandonCountersigningSession`]
125
+ * is made, signatures for this session come in. If they are valid, the session will be resolved and
126
+ * published as usual. Should they be invalid, however, the flag to abandon the session is erased.
127
+ * In such cases this request can be retried until the session has been abandoned successfully.
128
+ *
129
+ * # Errors
130
+ *
131
+ * [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
132
+ * passed in to the call.
133
+ *
134
+ * [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
135
+ * cell id.
136
+ *
137
+ * [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
138
+ * automatically has not been made.
139
+ */
140
+ abandonCountersigningSession(args: AbandonCountersigningSessionStateRequest): Promise<null>;
141
+ /**
142
+ * Publish an unresolved countersigning session.
143
+ *
144
+ * If the current session has not been resolved automatically, it can be forcefully published.
145
+ * A condition for this call to succeed is that at least one attempt has been made to resolve
146
+ * it automatically.
147
+ *
148
+ * # Returns
149
+ *
150
+ * [`AppResponse::PublishCountersigningSessionTriggered`]
151
+ *
152
+ * The session is marked for publishing and the countersigning workflow was triggered. The session
153
+ * has not been published yet.
154
+ *
155
+ * Upon successful publishing the system signal [`SystemSignal::SuccessfulCountersigning`] will
156
+ * be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
157
+ * would return `None`.
158
+ *
159
+ * In the countersigning workflow it will first be attempted to resolve the session with incoming
160
+ * signatures of the countersigned entries, before force-publishing the session. In a very rare event
161
+ * it could happen that in just the moment where the [`AppRequest::PublishCountersigningSession`]
162
+ * is made, signatures for this session come in. If they are valid, the session will be resolved and
163
+ * published as usual. Should they be invalid, however, the flag to publish the session is erased.
164
+ * In such cases this request can be retried until the session has been published successfully.
165
+ *
166
+ * # Errors
167
+ *
168
+ * [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
169
+ * passed in to the call.
170
+ *
171
+ * [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
172
+ * cell id.
173
+ *
174
+ * [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
175
+ * automatically has not been made.
176
+ */
177
+ publishCountersigningSession(args: PublishCountersigningSessionStateRequest): Promise<null>;
97
178
  /**
98
179
  * Register an event listener for signals.
99
180
  *
@@ -103,7 +184,6 @@ export declare class AppWebsocket implements AppClient {
103
184
  */
104
185
  on<Name extends keyof AppEvents>(eventName: Name | readonly Name[], listener: SignalCb): UnsubscribeFunction;
105
186
  private static requester;
106
- private containsCell;
107
187
  }
108
188
  /**
109
189
  * @public
@@ -1,6 +1,6 @@
1
1
  import Emittery from "emittery";
2
2
  import { omit } from "lodash-es";
3
- import { CellType } from "../admin/index.js";
3
+ import { CellType, } from "../admin/index.js";
4
4
  import { catchError, DEFAULT_TIMEOUT, getBaseRoleNameFromCloneId, HolochainError, isCloneId, promiseTimeout, requesterTransformer, } from "../common.js";
5
5
  import { getHostZomeCallSigner, getLauncherEnvironment, } from "../../environments/launcher.js";
6
6
  import { decode, encode } from "@msgpack/msgpack";
@@ -31,6 +31,9 @@ export class AppWebsocket {
31
31
  enableCloneCellRequester;
32
32
  disableCloneCellRequester;
33
33
  networkInfoRequester;
34
+ getCountersigningSessionStateRequester;
35
+ abandonCountersigningSessionRequester;
36
+ publishCountersigningSessionRequester;
34
37
  constructor(client, appInfo, callZomeTransform, defaultTimeout) {
35
38
  this.client = client;
36
39
  this.myPubKey = appInfo.agent_pub_key;
@@ -47,6 +50,9 @@ export class AppWebsocket {
47
50
  this.enableCloneCellRequester = AppWebsocket.requester(this.client, "enable_clone_cell", this.defaultTimeout);
48
51
  this.disableCloneCellRequester = AppWebsocket.requester(this.client, "disable_clone_cell", this.defaultTimeout);
49
52
  this.networkInfoRequester = AppWebsocket.requester(this.client, "network_info", this.defaultTimeout);
53
+ this.getCountersigningSessionStateRequester = AppWebsocket.requester(this.client, "get_countersigning_session_state", this.defaultTimeout);
54
+ this.abandonCountersigningSessionRequester = AppWebsocket.requester(this.client, "abandon_countersigning_session", this.defaultTimeout);
55
+ this.publishCountersigningSessionRequester = AppWebsocket.requester(this.client, "publish_countersigning_session", this.defaultTimeout);
50
56
  // Ensure all super methods are bound to this instance because Emittery relies on `this` being the instance.
51
57
  // Please retain until the upstream is fixed https://github.com/sindresorhus/emittery/issues/86.
52
58
  Object.getOwnPropertyNames(Emittery.prototype).forEach((name) => {
@@ -77,16 +83,10 @@ export class AppWebsocket {
77
83
  throw new HolochainError("ConnectionUrlMissing", `unable to connect to Conductor API - no url provided and not in a launcher environment.`);
78
84
  }
79
85
  const client = await WsClient.connect(options.url, options.wsClientOptions);
80
- if (env?.APP_INTERFACE_TOKEN) {
81
- // Note: This will only work for multiple connections if a single_use = false token is provided
82
- await client.authenticate({ token: env.APP_INTERFACE_TOKEN });
83
- }
84
- else {
85
- if (!options.token) {
86
- throw new HolochainError("AppAuthenticationTokenMissing", `unable to connect to Conductor API - no app authentication token provided.`);
87
- }
88
- await client.authenticate({ token: options.token });
89
- }
86
+ const token = options.token ?? env?.APP_INTERFACE_TOKEN;
87
+ if (!token)
88
+ throw new HolochainError("AppAuthenticationTokenMissing", `unable to connect to Conductor API - no app authentication token provided.`);
89
+ await client.authenticate({ token });
90
90
  const appInfo = await AppWebsocket.requester(client, "app_info", DEFAULT_TIMEOUT)(null);
91
91
  if (!appInfo) {
92
92
  throw new HolochainError("AppNotFound", `The app your connection token was issued for was not found. The app needs to be installed and enabled.`);
@@ -224,6 +224,90 @@ export class AppWebsocket {
224
224
  agent_pub_key: this.myPubKey,
225
225
  });
226
226
  }
227
+ /**
228
+ * Get the state of a countersigning session.
229
+ */
230
+ async getCountersigningSessionState(args) {
231
+ return this.getCountersigningSessionStateRequester(args);
232
+ }
233
+ /**
234
+ * Abandon an unresolved countersigning session.
235
+ *
236
+ * If the current session has not been resolved automatically, it can be forcefully abandoned.
237
+ * A condition for this call to succeed is that at least one attempt has been made to resolve
238
+ * it automatically.
239
+ *
240
+ * # Returns
241
+ *
242
+ * [`AppResponse::CountersigningSessionAbandoned`]
243
+ *
244
+ * The session is marked for abandoning and the countersigning workflow was triggered. The session
245
+ * has not been abandoned yet.
246
+ *
247
+ * Upon successful abandoning the system signal [`SystemSignal::AbandonedCountersigning`] will
248
+ * be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
249
+ * would return `None`.
250
+ *
251
+ * In the countersigning workflow it will first be attempted to resolve the session with incoming
252
+ * signatures of the countersigned entries, before force-abandoning the session. In a very rare event
253
+ * it could happen that in just the moment where the [`AppRequest::AbandonCountersigningSession`]
254
+ * is made, signatures for this session come in. If they are valid, the session will be resolved and
255
+ * published as usual. Should they be invalid, however, the flag to abandon the session is erased.
256
+ * In such cases this request can be retried until the session has been abandoned successfully.
257
+ *
258
+ * # Errors
259
+ *
260
+ * [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
261
+ * passed in to the call.
262
+ *
263
+ * [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
264
+ * cell id.
265
+ *
266
+ * [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
267
+ * automatically has not been made.
268
+ */
269
+ async abandonCountersigningSession(args) {
270
+ return this.abandonCountersigningSessionRequester(args);
271
+ }
272
+ /**
273
+ * Publish an unresolved countersigning session.
274
+ *
275
+ * If the current session has not been resolved automatically, it can be forcefully published.
276
+ * A condition for this call to succeed is that at least one attempt has been made to resolve
277
+ * it automatically.
278
+ *
279
+ * # Returns
280
+ *
281
+ * [`AppResponse::PublishCountersigningSessionTriggered`]
282
+ *
283
+ * The session is marked for publishing and the countersigning workflow was triggered. The session
284
+ * has not been published yet.
285
+ *
286
+ * Upon successful publishing the system signal [`SystemSignal::SuccessfulCountersigning`] will
287
+ * be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
288
+ * would return `None`.
289
+ *
290
+ * In the countersigning workflow it will first be attempted to resolve the session with incoming
291
+ * signatures of the countersigned entries, before force-publishing the session. In a very rare event
292
+ * it could happen that in just the moment where the [`AppRequest::PublishCountersigningSession`]
293
+ * is made, signatures for this session come in. If they are valid, the session will be resolved and
294
+ * published as usual. Should they be invalid, however, the flag to publish the session is erased.
295
+ * In such cases this request can be retried until the session has been published successfully.
296
+ *
297
+ * # Errors
298
+ *
299
+ * [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
300
+ * passed in to the call.
301
+ *
302
+ * [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
303
+ * cell id.
304
+ *
305
+ * [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
306
+ * automatically has not been made.
307
+ */
308
+ async publishCountersigningSession(args) {
309
+ return this.publishCountersigningSessionRequester(args);
310
+ }
227
311
  /**
228
312
  * Register an event listener for signals.
229
313
  *
@@ -237,25 +321,6 @@ export class AppWebsocket {
237
321
  static requester(client, tag, defaultTimeout, transformer) {
238
322
  return requesterTransformer((req, timeout) => promiseTimeout(client.request(req), tag, timeout || defaultTimeout).then(catchError), tag, transformer);
239
323
  }
240
- containsCell(cellId) {
241
- const appInfo = this.cachedAppInfo;
242
- if (!appInfo) {
243
- return false;
244
- }
245
- for (const roleName of Object.keys(appInfo.cell_info)) {
246
- for (const cellInfo of appInfo.cell_info[roleName]) {
247
- const currentCellId = CellType.Provisioned in cellInfo
248
- ? cellInfo[CellType.Provisioned].cell_id
249
- : CellType.Cloned in cellInfo
250
- ? cellInfo[CellType.Cloned].cell_id
251
- : undefined;
252
- if (currentCellId && isSameCell(currentCellId, cellId)) {
253
- return true;
254
- }
255
- }
256
- }
257
- return false;
258
- }
259
324
  }
260
325
  const defaultCallZomeTransform = {
261
326
  input: async (request) => {
@@ -272,8 +337,6 @@ const defaultCallZomeTransform = {
272
337
  },
273
338
  output: (response) => decode(response),
274
339
  };
275
- const isSameCell = (cellId1, cellId2) => cellId1[0].every((byte, index) => byte === cellId2[0][index]) &&
276
- cellId1[1].every((byte, index) => byte === cellId2[1][index]);
277
340
  /**
278
341
  * @public
279
342
  */
@@ -23,8 +23,8 @@ export declare class WsClient extends Emittery {
23
23
  options: WsClientOptions;
24
24
  private pendingRequests;
25
25
  private index;
26
+ private authenticationToken;
26
27
  constructor(socket: IsoWebSocket, url?: URL, options?: WsClientOptions);
27
- private setupSocket;
28
28
  /**
29
29
  * Instance factory for creating WsClients.
30
30
  *
@@ -47,6 +47,10 @@ export declare class WsClient extends Emittery {
47
47
  * @param request - The authentication request, containing an app authentication token.
48
48
  */
49
49
  authenticate(request: AppAuthenticationRequest): Promise<void>;
50
+ /**
51
+ * Close the websocket connection.
52
+ */
53
+ close(code?: number): Promise<IsoWebSocket.CloseEvent>;
50
54
  /**
51
55
  * Send requests to the connected websocket.
52
56
  *
@@ -56,10 +60,9 @@ export declare class WsClient extends Emittery {
56
60
  request<Response>(request: unknown): Promise<Response>;
57
61
  private exchange;
58
62
  private sendMessage;
63
+ private registerMessageListener;
64
+ private registerCloseListener;
65
+ private reconnectWebsocket;
59
66
  private handleResponse;
60
- /**
61
- * Close the websocket connection.
62
- */
63
- close(code?: number): Promise<CloseEvent>;
64
67
  }
65
68
  export { IsoWebSocket };
package/lib/api/client.js CHANGED
@@ -17,74 +17,16 @@ export class WsClient extends Emittery {
17
17
  options;
18
18
  pendingRequests;
19
19
  index;
20
+ authenticationToken;
20
21
  constructor(socket, url, options) {
21
22
  super();
23
+ this.registerMessageListener(socket);
24
+ this.registerCloseListener(socket);
22
25
  this.socket = socket;
23
26
  this.url = url;
24
27
  this.options = options || {};
25
28
  this.pendingRequests = {};
26
29
  this.index = 0;
27
- this.setupSocket();
28
- }
29
- setupSocket() {
30
- this.socket.onmessage = async (serializedMessage) => {
31
- // If data is not a buffer (nodejs), it will be a blob (browser)
32
- let deserializedData;
33
- if (globalThis.window &&
34
- serializedMessage.data instanceof globalThis.window.Blob) {
35
- deserializedData = await serializedMessage.data.arrayBuffer();
36
- }
37
- else {
38
- if (typeof Buffer !== "undefined" &&
39
- Buffer.isBuffer(serializedMessage.data)) {
40
- deserializedData = serializedMessage.data;
41
- }
42
- else {
43
- throw new HolochainError("UnknownMessageFormat", `incoming message has unknown message format - ${deserializedData}`);
44
- }
45
- }
46
- const message = decode(deserializedData);
47
- assertHolochainMessage(message);
48
- if (message.type === "signal") {
49
- if (message.data === null) {
50
- throw new HolochainError("UnknownSignalFormat", "incoming signal has no data");
51
- }
52
- const deserializedSignal = decode(message.data);
53
- assertHolochainSignal(deserializedSignal);
54
- if (SignalType.System in deserializedSignal) {
55
- this.emit("signal", {
56
- System: deserializedSignal[SignalType.System],
57
- });
58
- }
59
- else {
60
- const encodedAppSignal = deserializedSignal[SignalType.App];
61
- // In order to return readable content to the UI, the signal payload must also be deserialized.
62
- const payload = decode(encodedAppSignal.signal);
63
- const signal = {
64
- cell_id: encodedAppSignal.cell_id,
65
- zome_name: encodedAppSignal.zome_name,
66
- payload,
67
- };
68
- this.emit("signal", { App: signal });
69
- }
70
- }
71
- else if (message.type === "response") {
72
- this.handleResponse(message);
73
- }
74
- else {
75
- throw new HolochainError("UnknownMessageType", `incoming message has unknown type - ${message.type}`);
76
- }
77
- };
78
- this.socket.onclose = (event) => {
79
- const pendingRequestIds = Object.keys(this.pendingRequests).map((id) => parseInt(id));
80
- if (pendingRequestIds.length) {
81
- pendingRequestIds.forEach((id) => {
82
- const error = new HolochainError("ClientClosedWithPendingRequests", `client closed with pending requests - close event code: ${event.code}, request id: ${id}`);
83
- this.pendingRequests[id].reject(error);
84
- delete this.pendingRequests[id];
85
- });
86
- }
87
- };
88
30
  }
89
31
  /**
90
32
  * Instance factory for creating WsClients.
@@ -96,13 +38,13 @@ export class WsClient extends Emittery {
96
38
  static connect(url, options) {
97
39
  return new Promise((resolve, reject) => {
98
40
  const socket = new IsoWebSocket(url, options);
99
- socket.onerror = (errorEvent) => {
41
+ socket.addEventListener("error", (errorEvent) => {
100
42
  reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.error}`));
101
- };
102
- socket.onopen = () => {
43
+ });
44
+ socket.addEventListener("open", (_) => {
103
45
  const client = new WsClient(socket, url, options);
104
46
  resolve(client);
105
- };
47
+ }, { once: true });
106
48
  });
107
49
  }
108
50
  /**
@@ -125,16 +67,35 @@ export class WsClient extends Emittery {
125
67
  * @param request - The authentication request, containing an app authentication token.
126
68
  */
127
69
  async authenticate(request) {
128
- return this.exchange(request, (request, resolve) => {
70
+ this.authenticationToken = request.token;
71
+ return this.exchange(request, (request, resolve, reject) => {
72
+ const invalidTokenCloseListener = (closeEvent) => {
73
+ this.authenticationToken = undefined;
74
+ reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`));
75
+ };
76
+ this.socket.addEventListener("close", invalidTokenCloseListener, {
77
+ once: true,
78
+ });
129
79
  const encodedMsg = encode({
130
80
  type: "authenticate",
131
81
  data: encode(request),
132
82
  });
133
83
  this.socket.send(encodedMsg);
134
- // Message just needs to be sent first, no need to wait for a response or even require a flush
135
- resolve(null);
84
+ // Wait before resolving in case authentication fails.
85
+ setTimeout(() => {
86
+ this.socket.removeEventListener("close", invalidTokenCloseListener);
87
+ resolve(null);
88
+ }, 10);
136
89
  });
137
90
  }
91
+ /**
92
+ * Close the websocket connection.
93
+ */
94
+ close(code = 1000) {
95
+ const closedPromise = new Promise((resolve) => this.socket.addEventListener("close", (closeEvent) => resolve(closeEvent), { once: true }));
96
+ this.socket.close(code);
97
+ return closedPromise;
98
+ }
138
99
  /**
139
100
  * Send requests to the connected websocket.
140
101
  *
@@ -144,15 +105,22 @@ export class WsClient extends Emittery {
144
105
  async request(request) {
145
106
  return this.exchange(request, this.sendMessage.bind(this));
146
107
  }
147
- exchange(request, sendHandler) {
108
+ async exchange(request, sendHandler) {
148
109
  if (this.socket.readyState === this.socket.OPEN) {
149
110
  const promise = new Promise((resolve, reject) => {
150
111
  sendHandler(request, resolve, reject);
151
112
  });
152
113
  return promise;
153
114
  }
115
+ else if (this.url && this.authenticationToken) {
116
+ await this.reconnectWebsocket(this.url, this.authenticationToken);
117
+ this.registerMessageListener(this.socket);
118
+ this.registerCloseListener(this.socket);
119
+ const promise = new Promise((resolve, reject) => sendHandler(request, resolve, reject));
120
+ return promise;
121
+ }
154
122
  else {
155
- return Promise.reject(new Error("Socket is not open"));
123
+ return Promise.reject(new HolochainError("WebsocketClosedError", "Websocket is not open"));
156
124
  }
157
125
  }
158
126
  sendMessage(request, resolve, reject) {
@@ -166,6 +134,97 @@ export class WsClient extends Emittery {
166
134
  this.pendingRequests[id] = { resolve, reject };
167
135
  this.index += 1;
168
136
  }
137
+ registerMessageListener(socket) {
138
+ socket.onmessage = async (serializedMessage) => {
139
+ // If data is not a buffer (nodejs), it will be a blob (browser)
140
+ let deserializedData;
141
+ if (globalThis.window &&
142
+ serializedMessage.data instanceof globalThis.window.Blob) {
143
+ deserializedData = await serializedMessage.data.arrayBuffer();
144
+ }
145
+ else {
146
+ if (typeof Buffer !== "undefined" &&
147
+ Buffer.isBuffer(serializedMessage.data)) {
148
+ deserializedData = serializedMessage.data;
149
+ }
150
+ else {
151
+ throw new HolochainError("UnknownMessageFormat", `incoming message has unknown message format - ${deserializedData}`);
152
+ }
153
+ }
154
+ const message = decode(deserializedData);
155
+ assertHolochainMessage(message);
156
+ if (message.type === "signal") {
157
+ if (message.data === null) {
158
+ throw new HolochainError("UnknownSignalFormat", "incoming signal has no data");
159
+ }
160
+ const deserializedSignal = decode(message.data);
161
+ assertHolochainSignal(deserializedSignal);
162
+ if (SignalType.System in deserializedSignal) {
163
+ this.emit("signal", {
164
+ System: deserializedSignal[SignalType.System],
165
+ });
166
+ }
167
+ else {
168
+ const encodedAppSignal = deserializedSignal[SignalType.App];
169
+ // In order to return readable content to the UI, the signal payload must also be deserialized.
170
+ const payload = decode(encodedAppSignal.signal);
171
+ const signal = {
172
+ cell_id: encodedAppSignal.cell_id,
173
+ zome_name: encodedAppSignal.zome_name,
174
+ payload,
175
+ };
176
+ this.emit("signal", { App: signal });
177
+ }
178
+ }
179
+ else if (message.type === "response") {
180
+ this.handleResponse(message);
181
+ }
182
+ else {
183
+ throw new HolochainError("UnknownMessageType", `incoming message has unknown type - ${message.type}`);
184
+ }
185
+ };
186
+ }
187
+ registerCloseListener(socket) {
188
+ socket.addEventListener("close", (closeEvent) => {
189
+ const pendingRequestIds = Object.keys(this.pendingRequests).map((id) => parseInt(id));
190
+ if (pendingRequestIds.length) {
191
+ pendingRequestIds.forEach((id) => {
192
+ const error = new HolochainError("ClientClosedWithPendingRequests", `client closed with pending requests - close event code: ${closeEvent.code}, request id: ${id}`);
193
+ this.pendingRequests[id].reject(error);
194
+ delete this.pendingRequests[id];
195
+ });
196
+ }
197
+ }, { once: true });
198
+ }
199
+ async reconnectWebsocket(url, token) {
200
+ return new Promise((resolve, reject) => {
201
+ this.socket = new IsoWebSocket(url, this.options);
202
+ // This error event never occurs in tests. Could be removed?
203
+ this.socket.addEventListener("error", (errorEvent) => {
204
+ this.authenticationToken = undefined;
205
+ reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.message}`));
206
+ }, { once: true });
207
+ const invalidTokenCloseListener = (closeEvent) => {
208
+ this.authenticationToken = undefined;
209
+ reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`));
210
+ };
211
+ this.socket.addEventListener("close", invalidTokenCloseListener, {
212
+ once: true,
213
+ });
214
+ this.socket.addEventListener("open", async (_) => {
215
+ const encodedMsg = encode({
216
+ type: "authenticate",
217
+ data: encode({ token }),
218
+ });
219
+ this.socket.send(encodedMsg);
220
+ // Wait in case authentication fails.
221
+ setTimeout(() => {
222
+ this.socket.removeEventListener("close", invalidTokenCloseListener);
223
+ resolve();
224
+ }, 10);
225
+ }, { once: true });
226
+ });
227
+ }
169
228
  handleResponse(msg) {
170
229
  const id = msg.id;
171
230
  if (this.pendingRequests[id]) {
@@ -181,20 +240,6 @@ export class WsClient extends Emittery {
181
240
  console.error(`got response with no matching request. id = ${id} msg = ${msg}`);
182
241
  }
183
242
  }
184
- /**
185
- * Close the websocket connection.
186
- */
187
- close(code = 1000) {
188
- const closedPromise = new Promise((resolve) =>
189
- // for an unknown reason "addEventListener" is seen as a non-callable
190
- // property and gives a ts2349 error
191
- // type assertion as workaround
192
- this.socket.addEventListener("close", (event) => resolve(event))
193
- // }
194
- );
195
- this.socket.close(code);
196
- return closedPromise;
197
- }
198
243
  }
199
244
  function assertHolochainMessage(message) {
200
245
  if (typeof message === "object" &&
package/lib/api/common.js CHANGED
@@ -52,6 +52,7 @@ export const promiseTimeout = (promise, tag, ms) => {
52
52
  return res(a);
53
53
  })
54
54
  .catch((e) => {
55
+ clearTimeout(id);
55
56
  return rej(e);
56
57
  }));
57
58
  };
@@ -1,6 +1,13 @@
1
1
  import { AgentPubKey, DnaHash, EntryHash, ActionHash, HoloHashed, Signature, Timestamp } from "../types.js";
2
2
  import { Entry, EntryType } from "./entry.js";
3
3
  import { LinkTag, LinkType, RateWeight } from "./link.js";
4
+ /**
5
+ * @public
6
+ */
7
+ export interface SignedAction {
8
+ data: Action;
9
+ signature: Signature;
10
+ }
4
11
  /**
5
12
  * @public
6
13
  */
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.47.9"
8
+ "packageVersion": "7.47.11"
9
9
  }
10
10
  ]
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holochain/client",
3
- "version": "0.19.0-dev.0",
3
+ "version": "0.19.0-dev.2",
4
4
  "description": "A JavaScript client for the Holochain Conductor API",
5
5
  "author": "Holochain Foundation <info@holochain.org> (https://holochain.org)",
6
6
  "license": "CAL-1.0",
@@ -72,4 +72,4 @@
72
72
  "tsx": "^4.7.2",
73
73
  "typescript": "^4.9.5"
74
74
  }
75
- }
75
+ }