@vinaverse/client 0.1.0-alpha.2 → 0.1.0-alpha.4
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/dist/{chunk-AWHPWRGT.js → chunk-MKUNSAMT.js} +73 -11
- package/dist/chunk-MKUNSAMT.js.map +1 -0
- package/dist/conversation/index.cjs +241 -28
- package/dist/conversation/index.cjs.map +1 -1
- package/dist/conversation/index.d.cts +71 -6
- package/dist/conversation/index.d.ts +71 -6
- package/dist/conversation/index.js +173 -23
- package/dist/conversation/index.js.map +1 -1
- package/dist/index.cjs +70 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{session--W6JXfwo.d.cts → session-C3qZwGPG.d.cts} +130 -7
- package/dist/{session--W6JXfwo.d.ts → session-C3qZwGPG.d.ts} +130 -7
- package/package.json +4 -4
- package/dist/chunk-AWHPWRGT.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Q as Question, A as AnswerInput, H as HeldCard, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session
|
|
2
|
-
export { b as CONTRACT, c as Card, d as
|
|
1
|
+
import { Q as Question, A as AnswerInput, H as HeldCard, W as Welcome, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session-C3qZwGPG.cjs';
|
|
2
|
+
export { b as CONTRACT, c as Card, d as CardGroup, e as CardImage, f as CardKind, g as CardLink, h as ClientErrorCode, i as HeldQuestion, j as Proposal, V as VinaverseClientError, k as isKnownCard } from '../session-C3qZwGPG.cjs';
|
|
3
3
|
|
|
4
4
|
type AnswerProblem =
|
|
5
5
|
/** Nothing chosen or typed, and the question is required (or skipped while required). */
|
|
@@ -47,7 +47,10 @@ type ConversationStatus =
|
|
|
47
47
|
| 'sse'
|
|
48
48
|
/** The session ended; `next` says how the conversation goes on. */
|
|
49
49
|
| 'ended'
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The client secret was refused, and the new session the store then asked for was refused as well: `start()` (the
|
|
52
|
+
* person's "Try again") asks for another.
|
|
53
|
+
*/
|
|
51
54
|
| 'unauthorized'
|
|
52
55
|
/** `dispose()` or `clear()` closed it. */
|
|
53
56
|
| 'closed';
|
|
@@ -102,13 +105,17 @@ interface SystemItem {
|
|
|
102
105
|
readonly event: SystemEvent;
|
|
103
106
|
/**
|
|
104
107
|
* `takeover_started`: `by`; `handoff_created`: `expected_response`; `flow_completed`: `result`, `code`;
|
|
105
|
-
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable
|
|
108
|
+
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable`, `turn` (its `turn_id`), and `retry`:
|
|
109
|
+
* the server allows a retry and the person's text that started the turn is known, so `retryTurn(key)` sends it
|
|
110
|
+
* again (false once it did).
|
|
106
111
|
*/
|
|
107
112
|
readonly data: Readonly<Record<string, string | boolean | null>>;
|
|
108
113
|
}
|
|
109
114
|
type TimelineItem = MessageItem | CardItem | SystemItem;
|
|
110
115
|
interface ConversationView {
|
|
111
116
|
readonly status: ConversationStatus;
|
|
117
|
+
/** The conversation the sessions belong to, once a session named it; for the tenant's own records and support. */
|
|
118
|
+
readonly conversationId: string | null;
|
|
112
119
|
readonly banner: ConversationBanner;
|
|
113
120
|
/**
|
|
114
121
|
* How the conversation goes on after the session ended: `continue` on the person's next action (it resumes on its
|
|
@@ -119,6 +126,11 @@ interface ConversationView {
|
|
|
119
126
|
readonly name: string | null;
|
|
120
127
|
readonly avatarUrl: string | null;
|
|
121
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* The tenant's greeting and chips for the session's mode (v1-draft.20, `Session.welcome`), or null. A UI shows it on
|
|
131
|
+
* an empty conversation; a chip sends its `message` as the person's own text (`send`).
|
|
132
|
+
*/
|
|
133
|
+
readonly welcome: Welcome | null;
|
|
122
134
|
/** The server's AI disclosure; null until the first one, when the UI shows its own line. Never hidden. */
|
|
123
135
|
readonly disclosure: string | null;
|
|
124
136
|
/** While a person from the business replies: their role (`by`, never a name). */
|
|
@@ -179,6 +191,13 @@ type ConversationSession = SessionInfo & {
|
|
|
179
191
|
readonly avatar_url?: string;
|
|
180
192
|
};
|
|
181
193
|
readonly disclosure?: string;
|
|
194
|
+
/**
|
|
195
|
+
* When the session ends (`Session.expires_at`: the earliest of its own limit and the tenant's tool token). Past it,
|
|
196
|
+
* the conversation goes on in a new session from `getSession`, never with a fresh secret for this one.
|
|
197
|
+
*/
|
|
198
|
+
readonly expires_at?: string;
|
|
199
|
+
/** v1-draft.20: the tenant's greeting and chips for the session's mode (`Session.welcome`); no model call. */
|
|
200
|
+
readonly welcome?: Welcome;
|
|
182
201
|
};
|
|
183
202
|
/**
|
|
184
203
|
* Runs a callback before the next frame; `requestAnimationFrame` where there is one. Every listener, of the view and
|
|
@@ -249,6 +268,8 @@ declare class ConversationStore {
|
|
|
249
268
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
250
269
|
private readonly decisionErrors;
|
|
251
270
|
private system;
|
|
271
|
+
/** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
|
|
272
|
+
private readonly turnInputs;
|
|
252
273
|
/** Numbers system rows that carry no id of their own. */
|
|
253
274
|
private systemSeq;
|
|
254
275
|
private history;
|
|
@@ -259,6 +280,8 @@ declare class ConversationStore {
|
|
|
259
280
|
private readySession;
|
|
260
281
|
private cardOrder;
|
|
261
282
|
private kept;
|
|
283
|
+
/** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
|
|
284
|
+
private readonly inert;
|
|
262
285
|
private locale;
|
|
263
286
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
264
287
|
private fetching;
|
|
@@ -297,7 +320,12 @@ declare class ConversationStore {
|
|
|
297
320
|
/** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
|
|
298
321
|
private suspended;
|
|
299
322
|
private suspend;
|
|
300
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
|
|
325
|
+
* when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
|
|
326
|
+
* back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
|
|
327
|
+
* and until then nothing connects.
|
|
328
|
+
*/
|
|
301
329
|
private wake;
|
|
302
330
|
/** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
|
|
303
331
|
setOnline(online: boolean | null): void;
|
|
@@ -321,6 +349,12 @@ declare class ConversationStore {
|
|
|
321
349
|
decideProposal(cardId: string, decide: () => Promise<ProposalOutcome>): Promise<ProposalOutcome | null>;
|
|
322
350
|
/** Send a "not sent" message again, as a new message (the person's action). Returns the new id. */
|
|
323
351
|
retry(key: string): string | null;
|
|
352
|
+
/**
|
|
353
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
354
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
355
|
+
* allows no retry.
|
|
356
|
+
*/
|
|
357
|
+
retryTurn(key: string): string | null;
|
|
324
358
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
325
359
|
continueHere(): Promise<void>;
|
|
326
360
|
/** Stop the turn in progress. */
|
|
@@ -336,8 +370,12 @@ declare class ConversationStore {
|
|
|
336
370
|
/** The person acted after the session ended idle, expired or at its limit: the next session takes what they sent. */
|
|
337
371
|
private continueIfEnded;
|
|
338
372
|
private reconnectIfDown;
|
|
373
|
+
/** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
|
|
374
|
+
private rolled;
|
|
339
375
|
private statusChanged;
|
|
340
376
|
private event;
|
|
377
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
378
|
+
private turnText;
|
|
341
379
|
/**
|
|
342
380
|
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
343
381
|
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
@@ -362,10 +400,37 @@ declare class ConversationStore {
|
|
|
362
400
|
private key;
|
|
363
401
|
private restore;
|
|
364
402
|
private schedulePersist;
|
|
403
|
+
/** Past the session's end (`expires_at`), when the backend passed it on. */
|
|
404
|
+
private sessionOver;
|
|
365
405
|
private live;
|
|
366
406
|
private required;
|
|
367
407
|
}
|
|
368
408
|
/** One conversation with an agent, for any UI: see `ConversationStore`. */
|
|
369
409
|
declare function createConversation(options: ConversationOptions): ConversationStore;
|
|
370
410
|
|
|
371
|
-
|
|
411
|
+
/** A session request, with the mode it is for. */
|
|
412
|
+
interface ModeSessionRequest extends SessionRequest {
|
|
413
|
+
readonly mode: string;
|
|
414
|
+
}
|
|
415
|
+
interface ConversationsByModeOptions extends Omit<ConversationOptions, 'getSession'> {
|
|
416
|
+
/** Your backend creates the session for the mode's agent (its `subject.type`, its agent), with its service key. */
|
|
417
|
+
readonly getSession: (request: ModeSessionRequest) => Promise<ConversationSession>;
|
|
418
|
+
/** Every mode the app has, so `clear()` forgets each one's kept history, even one not opened since launch. */
|
|
419
|
+
readonly modes?: readonly string[];
|
|
420
|
+
}
|
|
421
|
+
interface ConversationsByMode {
|
|
422
|
+
/** The mode's store, made the first time it is asked for; the same store after that. */
|
|
423
|
+
get(mode: string): ConversationStore;
|
|
424
|
+
/**
|
|
425
|
+
* The mode the person switched to: its store, with every other mode's sent to the background (no pings; closed
|
|
426
|
+
* with `closeInBackground`). The kit then drives the selected one with the app's foreground and background.
|
|
427
|
+
*/
|
|
428
|
+
select(mode: string): ConversationStore;
|
|
429
|
+
/** Close every mode's store. */
|
|
430
|
+
dispose(): void;
|
|
431
|
+
/** Close every mode's store and forget what each kept on the device (sign-out). */
|
|
432
|
+
clear(): void;
|
|
433
|
+
}
|
|
434
|
+
declare function createConversationsByMode(options: ConversationsByModeOptions): ConversationsByMode;
|
|
435
|
+
|
|
436
|
+
export { AnswerInput, type AnswerProblem, type CardItem, type ConversationBanner, type ConversationOptions, type ConversationPersistence, type ConversationSession, type ConversationStatus, ConversationStore, type ConversationView, type ConversationsByMode, type ConversationsByModeOptions, type FrameScheduler, HeldCard, MAX_TEXT, type MessageItem, type MessageState, type ModeSessionRequest, ProposalOutcome, Question, type SessionReason, type SessionRequest, type StreamingText, type SystemEvent, type SystemItem, type TimelineItem, ToolLine, Welcome, answerProblem, codePoints, createConversation, createConversationsByMode, isAllowedLink, isValidLinkPrefix };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Q as Question, A as AnswerInput, H as HeldCard, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session
|
|
2
|
-
export { b as CONTRACT, c as Card, d as
|
|
1
|
+
import { Q as Question, A as AnswerInput, H as HeldCard, W as Welcome, T as ToolLine, S as SessionInfo, C as ClientSecret, a as SessionOptions, P as ProposalOutcome } from '../session-C3qZwGPG.js';
|
|
2
|
+
export { b as CONTRACT, c as Card, d as CardGroup, e as CardImage, f as CardKind, g as CardLink, h as ClientErrorCode, i as HeldQuestion, j as Proposal, V as VinaverseClientError, k as isKnownCard } from '../session-C3qZwGPG.js';
|
|
3
3
|
|
|
4
4
|
type AnswerProblem =
|
|
5
5
|
/** Nothing chosen or typed, and the question is required (or skipped while required). */
|
|
@@ -47,7 +47,10 @@ type ConversationStatus =
|
|
|
47
47
|
| 'sse'
|
|
48
48
|
/** The session ended; `next` says how the conversation goes on. */
|
|
49
49
|
| 'ended'
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The client secret was refused, and the new session the store then asked for was refused as well: `start()` (the
|
|
52
|
+
* person's "Try again") asks for another.
|
|
53
|
+
*/
|
|
51
54
|
| 'unauthorized'
|
|
52
55
|
/** `dispose()` or `clear()` closed it. */
|
|
53
56
|
| 'closed';
|
|
@@ -102,13 +105,17 @@ interface SystemItem {
|
|
|
102
105
|
readonly event: SystemEvent;
|
|
103
106
|
/**
|
|
104
107
|
* `takeover_started`: `by`; `handoff_created`: `expected_response`; `flow_completed`: `result`, `code`;
|
|
105
|
-
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable
|
|
108
|
+
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable`, `turn` (its `turn_id`), and `retry`:
|
|
109
|
+
* the server allows a retry and the person's text that started the turn is known, so `retryTurn(key)` sends it
|
|
110
|
+
* again (false once it did).
|
|
106
111
|
*/
|
|
107
112
|
readonly data: Readonly<Record<string, string | boolean | null>>;
|
|
108
113
|
}
|
|
109
114
|
type TimelineItem = MessageItem | CardItem | SystemItem;
|
|
110
115
|
interface ConversationView {
|
|
111
116
|
readonly status: ConversationStatus;
|
|
117
|
+
/** The conversation the sessions belong to, once a session named it; for the tenant's own records and support. */
|
|
118
|
+
readonly conversationId: string | null;
|
|
112
119
|
readonly banner: ConversationBanner;
|
|
113
120
|
/**
|
|
114
121
|
* How the conversation goes on after the session ended: `continue` on the person's next action (it resumes on its
|
|
@@ -119,6 +126,11 @@ interface ConversationView {
|
|
|
119
126
|
readonly name: string | null;
|
|
120
127
|
readonly avatarUrl: string | null;
|
|
121
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* The tenant's greeting and chips for the session's mode (v1-draft.20, `Session.welcome`), or null. A UI shows it on
|
|
131
|
+
* an empty conversation; a chip sends its `message` as the person's own text (`send`).
|
|
132
|
+
*/
|
|
133
|
+
readonly welcome: Welcome | null;
|
|
122
134
|
/** The server's AI disclosure; null until the first one, when the UI shows its own line. Never hidden. */
|
|
123
135
|
readonly disclosure: string | null;
|
|
124
136
|
/** While a person from the business replies: their role (`by`, never a name). */
|
|
@@ -179,6 +191,13 @@ type ConversationSession = SessionInfo & {
|
|
|
179
191
|
readonly avatar_url?: string;
|
|
180
192
|
};
|
|
181
193
|
readonly disclosure?: string;
|
|
194
|
+
/**
|
|
195
|
+
* When the session ends (`Session.expires_at`: the earliest of its own limit and the tenant's tool token). Past it,
|
|
196
|
+
* the conversation goes on in a new session from `getSession`, never with a fresh secret for this one.
|
|
197
|
+
*/
|
|
198
|
+
readonly expires_at?: string;
|
|
199
|
+
/** v1-draft.20: the tenant's greeting and chips for the session's mode (`Session.welcome`); no model call. */
|
|
200
|
+
readonly welcome?: Welcome;
|
|
182
201
|
};
|
|
183
202
|
/**
|
|
184
203
|
* Runs a callback before the next frame; `requestAnimationFrame` where there is one. Every listener, of the view and
|
|
@@ -249,6 +268,8 @@ declare class ConversationStore {
|
|
|
249
268
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
250
269
|
private readonly decisionErrors;
|
|
251
270
|
private system;
|
|
271
|
+
/** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
|
|
272
|
+
private readonly turnInputs;
|
|
252
273
|
/** Numbers system rows that carry no id of their own. */
|
|
253
274
|
private systemSeq;
|
|
254
275
|
private history;
|
|
@@ -259,6 +280,8 @@ declare class ConversationStore {
|
|
|
259
280
|
private readySession;
|
|
260
281
|
private cardOrder;
|
|
261
282
|
private kept;
|
|
283
|
+
/** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
|
|
284
|
+
private readonly inert;
|
|
262
285
|
private locale;
|
|
263
286
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
264
287
|
private fetching;
|
|
@@ -297,7 +320,12 @@ declare class ConversationStore {
|
|
|
297
320
|
/** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
|
|
298
321
|
private suspended;
|
|
299
322
|
private suspend;
|
|
300
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
|
|
325
|
+
* when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
|
|
326
|
+
* back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
|
|
327
|
+
* and until then nothing connects.
|
|
328
|
+
*/
|
|
301
329
|
private wake;
|
|
302
330
|
/** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
|
|
303
331
|
setOnline(online: boolean | null): void;
|
|
@@ -321,6 +349,12 @@ declare class ConversationStore {
|
|
|
321
349
|
decideProposal(cardId: string, decide: () => Promise<ProposalOutcome>): Promise<ProposalOutcome | null>;
|
|
322
350
|
/** Send a "not sent" message again, as a new message (the person's action). Returns the new id. */
|
|
323
351
|
retry(key: string): string | null;
|
|
352
|
+
/**
|
|
353
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
354
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
355
|
+
* allows no retry.
|
|
356
|
+
*/
|
|
357
|
+
retryTurn(key: string): string | null;
|
|
324
358
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
325
359
|
continueHere(): Promise<void>;
|
|
326
360
|
/** Stop the turn in progress. */
|
|
@@ -336,8 +370,12 @@ declare class ConversationStore {
|
|
|
336
370
|
/** The person acted after the session ended idle, expired or at its limit: the next session takes what they sent. */
|
|
337
371
|
private continueIfEnded;
|
|
338
372
|
private reconnectIfDown;
|
|
373
|
+
/** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
|
|
374
|
+
private rolled;
|
|
339
375
|
private statusChanged;
|
|
340
376
|
private event;
|
|
377
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
378
|
+
private turnText;
|
|
341
379
|
/**
|
|
342
380
|
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
343
381
|
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
@@ -362,10 +400,37 @@ declare class ConversationStore {
|
|
|
362
400
|
private key;
|
|
363
401
|
private restore;
|
|
364
402
|
private schedulePersist;
|
|
403
|
+
/** Past the session's end (`expires_at`), when the backend passed it on. */
|
|
404
|
+
private sessionOver;
|
|
365
405
|
private live;
|
|
366
406
|
private required;
|
|
367
407
|
}
|
|
368
408
|
/** One conversation with an agent, for any UI: see `ConversationStore`. */
|
|
369
409
|
declare function createConversation(options: ConversationOptions): ConversationStore;
|
|
370
410
|
|
|
371
|
-
|
|
411
|
+
/** A session request, with the mode it is for. */
|
|
412
|
+
interface ModeSessionRequest extends SessionRequest {
|
|
413
|
+
readonly mode: string;
|
|
414
|
+
}
|
|
415
|
+
interface ConversationsByModeOptions extends Omit<ConversationOptions, 'getSession'> {
|
|
416
|
+
/** Your backend creates the session for the mode's agent (its `subject.type`, its agent), with its service key. */
|
|
417
|
+
readonly getSession: (request: ModeSessionRequest) => Promise<ConversationSession>;
|
|
418
|
+
/** Every mode the app has, so `clear()` forgets each one's kept history, even one not opened since launch. */
|
|
419
|
+
readonly modes?: readonly string[];
|
|
420
|
+
}
|
|
421
|
+
interface ConversationsByMode {
|
|
422
|
+
/** The mode's store, made the first time it is asked for; the same store after that. */
|
|
423
|
+
get(mode: string): ConversationStore;
|
|
424
|
+
/**
|
|
425
|
+
* The mode the person switched to: its store, with every other mode's sent to the background (no pings; closed
|
|
426
|
+
* with `closeInBackground`). The kit then drives the selected one with the app's foreground and background.
|
|
427
|
+
*/
|
|
428
|
+
select(mode: string): ConversationStore;
|
|
429
|
+
/** Close every mode's store. */
|
|
430
|
+
dispose(): void;
|
|
431
|
+
/** Close every mode's store and forget what each kept on the device (sign-out). */
|
|
432
|
+
clear(): void;
|
|
433
|
+
}
|
|
434
|
+
declare function createConversationsByMode(options: ConversationsByModeOptions): ConversationsByMode;
|
|
435
|
+
|
|
436
|
+
export { AnswerInput, type AnswerProblem, type CardItem, type ConversationBanner, type ConversationOptions, type ConversationPersistence, type ConversationSession, type ConversationStatus, ConversationStore, type ConversationView, type ConversationsByMode, type ConversationsByModeOptions, type FrameScheduler, HeldCard, MAX_TEXT, type MessageItem, type MessageState, type ModeSessionRequest, ProposalOutcome, Question, type SessionReason, type SessionRequest, type StreamingText, type SystemEvent, type SystemItem, type TimelineItem, ToolLine, Welcome, answerProblem, codePoints, createConversation, createConversationsByMode, isAllowedLink, isValidLinkPrefix };
|