@datalayer/agent-runtimes 1.5.24 → 1.5.25

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The protocol displays a run's tool results carry, read strictly for any
3
+ * renderer (MOBILE M-037, REACTOR_UI RU-1): the page's half of a tool result
4
+ * — `_meta["reactor.ui/display"]`, as the runtime's AG-UI stream sends it
5
+ * beside the model's view — decoded into what a card draws.
6
+ *
7
+ * - **Sign in at a company** (`…pap.sign-in+json`, and a plain `pap_sign_in`
8
+ * or a business's `pact_sign_in` answer): Direct, Device or Mediated, its
9
+ * page (`https` only), its code, until when, what it needs.
10
+ * - **An operation to confirm** (`…pap.confirm+json`): the company's words
11
+ * verbatim, its terms, the revision and the one it replaced, what it does
12
+ * (a Rules class; `buy` when money moves — a purchase), whether only the
13
+ * person may decide. It is answered where approvals are, never here: the
14
+ * approval it stands for is named (`approvalId`).
15
+ * - **What a confirmed operation came to** (`…pap.outcome+json`).
16
+ * - **What a company answered** (`…pap.replies+json`): each reply with who
17
+ * wrote it there (its agent or one of its people), who answers now and
18
+ * where the conversation stands — participation.
19
+ * - **A handoff** (`pap_handoff`'s answer): handed to the company's people,
20
+ * or why not.
21
+ *
22
+ * Anything else — an unknown type, a known one whose data does not read — is
23
+ * `unknown`: its type, version and model-safe text, nothing drawn from its
24
+ * data (diagnosable and safe). Typed decoding fails closed.
25
+ *
26
+ * @module portable/displays
27
+ */
28
+ import { type PapSignInWaiting } from './signIn';
29
+ export declare const PAP_SIGN_IN_TYPE = "application/vnd.datalayer.pap.sign-in+json";
30
+ export declare const PAP_CONFIRM_TYPE = "application/vnd.datalayer.pap.confirm+json";
31
+ export declare const PAP_OUTCOME_TYPE = "application/vnd.datalayer.pap.outcome+json";
32
+ export declare const PAP_REPLIES_TYPE = "application/vnd.datalayer.pap.replies+json";
33
+ /** Where a tool result carries its display for the page. */
34
+ export declare const DISPLAY_META_KEY = "reactor.ui/display";
35
+ /** What confirming an operation does, as an application's Rules class it. */
36
+ export type ActionClass = 'read' | 'write' | 'send' | 'buy' | 'delete' | 'publish';
37
+ export type ProtocolDisplay = ({
38
+ kind: 'sign-in';
39
+ id: string;
40
+ } & PapSignInWaiting) | {
41
+ kind: 'sign-in-refused';
42
+ id: string;
43
+ why: string;
44
+ } | {
45
+ kind: 'confirm';
46
+ id: string;
47
+ /** The approval it stands for: answered where approvals are. */
48
+ approvalId: string;
49
+ company: string;
50
+ summary: string;
51
+ terms: Record<string, unknown>;
52
+ operationRef: string;
53
+ revision: number;
54
+ does: ActionClass | null;
55
+ signedIn: boolean | null;
56
+ expiresAt: string;
57
+ was: {
58
+ revision: number;
59
+ terms: Record<string, unknown>;
60
+ } | null;
61
+ onlyThePerson: boolean;
62
+ } | {
63
+ kind: 'outcome';
64
+ id: string;
65
+ company: string;
66
+ summary: string;
67
+ operationRef: string;
68
+ revision: number;
69
+ state: 'in_progress' | 'succeeded' | 'failed' | 'cancelled' | 'expired';
70
+ result: string;
71
+ approvedBy: 'user' | 'standing_permission' | null;
72
+ } | {
73
+ kind: 'replies';
74
+ id: string;
75
+ company: string;
76
+ conversation: string;
77
+ replies: {
78
+ sender: 'agent' | 'human';
79
+ text: string;
80
+ }[];
81
+ status: 'working' | 'idle' | 'queued' | 'closed' | null;
82
+ responder: 'agent' | 'human' | null;
83
+ } | {
84
+ kind: 'handoff';
85
+ id: string;
86
+ handed: boolean;
87
+ responder: 'agent' | 'human' | null;
88
+ note: string;
89
+ } | {
90
+ kind: 'unknown';
91
+ id: string;
92
+ type: string;
93
+ version: number;
94
+ text: string;
95
+ };
96
+ /** A display's wire (`{type, version, id, text, data}`) decoded, or `unknown` with its text. */
97
+ export declare function protocolDisplayOf(wire: unknown): ProtocolDisplay | null;
98
+ /** `pap_handoff`, as the gateway names it with or without a prefix. */
99
+ export declare const PAP_HANDOFF_TOOL: RegExp;
100
+ /**
101
+ * The protocol display of a tool's result, or `null` for one that has none:
102
+ * its embedded display first; else a plain `pap_sign_in`/`pact_sign_in`
103
+ * answer or a `pap_handoff` answer, read by the tool's name.
104
+ */
105
+ export declare function displayOfToolResult(toolName: string, result: unknown, id?: string): ProtocolDisplay | null;
106
+ /** What each display says in one line, for a screen reader and a collapsed row. */
107
+ export declare function displayLine(display: ProtocolDisplay): string;
108
+ /** Who wrote a reply, said apart from the person's words and the agent's summaries (PAP-7.8-01). */
109
+ export declare const replyAuthor: (company: string, sender: "agent" | "human") => string;
@@ -0,0 +1,261 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The protocol displays a run's tool results carry, read strictly for any
7
+ * renderer (MOBILE M-037, REACTOR_UI RU-1): the page's half of a tool result
8
+ * — `_meta["reactor.ui/display"]`, as the runtime's AG-UI stream sends it
9
+ * beside the model's view — decoded into what a card draws.
10
+ *
11
+ * - **Sign in at a company** (`…pap.sign-in+json`, and a plain `pap_sign_in`
12
+ * or a business's `pact_sign_in` answer): Direct, Device or Mediated, its
13
+ * page (`https` only), its code, until when, what it needs.
14
+ * - **An operation to confirm** (`…pap.confirm+json`): the company's words
15
+ * verbatim, its terms, the revision and the one it replaced, what it does
16
+ * (a Rules class; `buy` when money moves — a purchase), whether only the
17
+ * person may decide. It is answered where approvals are, never here: the
18
+ * approval it stands for is named (`approvalId`).
19
+ * - **What a confirmed operation came to** (`…pap.outcome+json`).
20
+ * - **What a company answered** (`…pap.replies+json`): each reply with who
21
+ * wrote it there (its agent or one of its people), who answers now and
22
+ * where the conversation stands — participation.
23
+ * - **A handoff** (`pap_handoff`'s answer): handed to the company's people,
24
+ * or why not.
25
+ *
26
+ * Anything else — an unknown type, a known one whose data does not read — is
27
+ * `unknown`: its type, version and model-safe text, nothing drawn from its
28
+ * data (diagnosable and safe). Typed decoding fails closed.
29
+ *
30
+ * @module portable/displays
31
+ */
32
+ import { isPapSignInTool, readPapSignIn, } from './signIn';
33
+ export const PAP_SIGN_IN_TYPE = 'application/vnd.datalayer.pap.sign-in+json';
34
+ export const PAP_CONFIRM_TYPE = 'application/vnd.datalayer.pap.confirm+json';
35
+ export const PAP_OUTCOME_TYPE = 'application/vnd.datalayer.pap.outcome+json';
36
+ export const PAP_REPLIES_TYPE = 'application/vnd.datalayer.pap.replies+json';
37
+ /** Where a tool result carries its display for the page. */
38
+ export const DISPLAY_META_KEY = 'reactor.ui/display';
39
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value)
40
+ ? value
41
+ : null;
42
+ const text = (value) => typeof value === 'string' ? value : '';
43
+ const words = (value) => text(value).trim();
44
+ const count = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 1
45
+ ? value
46
+ : 0;
47
+ const ACTIONS = [
48
+ 'read',
49
+ 'write',
50
+ 'send',
51
+ 'buy',
52
+ 'delete',
53
+ 'publish',
54
+ ];
55
+ const OUTCOMES = [
56
+ 'in_progress',
57
+ 'succeeded',
58
+ 'failed',
59
+ 'cancelled',
60
+ 'expired',
61
+ ];
62
+ const STATUSES = ['working', 'idle', 'queued', 'closed'];
63
+ const who = (value) => value === 'agent' || value === 'human' ? value : null;
64
+ /** A display's wire (`{type, version, id, text, data}`) decoded, or `unknown` with its text. */
65
+ export function protocolDisplayOf(wire) {
66
+ const shown = record(wire);
67
+ if (!shown || !text(shown.type)) {
68
+ return null;
69
+ }
70
+ const id = text(shown.id);
71
+ const data = record(shown.data) ?? {};
72
+ const unknown = {
73
+ kind: 'unknown',
74
+ id,
75
+ type: text(shown.type),
76
+ version: typeof shown.version === 'number' ? shown.version : 0,
77
+ text: text(shown.text),
78
+ };
79
+ switch (shown.type) {
80
+ case PAP_SIGN_IN_TYPE: {
81
+ const read = readPapSignIn({ status: 'waiting_for_the_person', ...data });
82
+ if (!read)
83
+ return unknown;
84
+ return 'refused' in read
85
+ ? { kind: 'sign-in-refused', id, why: read.refused }
86
+ : { kind: 'sign-in', id, ...read };
87
+ }
88
+ case PAP_CONFIRM_TYPE: {
89
+ const company = words(data.company);
90
+ const summary = text(data.summary);
91
+ const operationRef = words(data.operation_ref);
92
+ const revision = count(data.revision);
93
+ const terms = record(data.terms);
94
+ const was = record(data.was);
95
+ const wasRevision = was ? count(was.revision) : 0;
96
+ if (!company ||
97
+ !summary.trim() ||
98
+ !operationRef ||
99
+ !revision ||
100
+ !terms ||
101
+ !id.endsWith('-confirm')) {
102
+ return unknown;
103
+ }
104
+ if (was && (!wasRevision || wasRevision >= revision)) {
105
+ return unknown;
106
+ }
107
+ const does = data.does === null || data.does === undefined
108
+ ? null
109
+ : ACTIONS.find(a => a === data.does);
110
+ if (does === undefined)
111
+ return unknown;
112
+ const session = record(data.session);
113
+ return {
114
+ kind: 'confirm',
115
+ id,
116
+ approvalId: id.slice(0, -'-confirm'.length),
117
+ company,
118
+ summary,
119
+ terms,
120
+ operationRef,
121
+ revision,
122
+ does,
123
+ signedIn: session && typeof session.signed_in === 'boolean'
124
+ ? session.signed_in
125
+ : null,
126
+ expiresAt: text(data.expires_at),
127
+ was: was
128
+ ? { revision: wasRevision, terms: record(was.terms) ?? {} }
129
+ : null,
130
+ onlyThePerson: data.only_the_person === true,
131
+ };
132
+ }
133
+ case PAP_OUTCOME_TYPE: {
134
+ const state = OUTCOMES.find(s => s === data.state);
135
+ const company = words(data.company);
136
+ const summary = text(data.summary);
137
+ const result = text(data.result);
138
+ if (!state ||
139
+ !company ||
140
+ !summary.trim() ||
141
+ !words(data.operation_ref) ||
142
+ !count(data.revision)) {
143
+ return unknown;
144
+ }
145
+ // Final, it says what it came to in the company's words; in progress, nothing yet.
146
+ if ((state === 'in_progress') !== !result.trim())
147
+ return unknown;
148
+ return {
149
+ kind: 'outcome',
150
+ id,
151
+ company,
152
+ summary,
153
+ operationRef: words(data.operation_ref),
154
+ revision: count(data.revision),
155
+ state,
156
+ result,
157
+ approvedBy: data.approved_by === 'user' ||
158
+ data.approved_by === 'standing_permission'
159
+ ? data.approved_by
160
+ : null,
161
+ };
162
+ }
163
+ case PAP_REPLIES_TYPE: {
164
+ const company = words(data.company);
165
+ const conversation = text(data.conversation);
166
+ const replies = Array.isArray(data.replies)
167
+ ? data.replies.map(record).map(reply => ({
168
+ sender: who(reply?.sender),
169
+ text: text(reply?.text),
170
+ }))
171
+ : [];
172
+ if (!company ||
173
+ !/^[A-Za-z0-9_-]{1,256}$/.test(conversation) ||
174
+ !replies.length ||
175
+ replies.some(reply => !reply.sender || !reply.text.trim())) {
176
+ return unknown;
177
+ }
178
+ return {
179
+ kind: 'replies',
180
+ id,
181
+ company,
182
+ conversation,
183
+ replies: replies,
184
+ status: STATUSES.find(s => s === data.status) ?? null,
185
+ responder: who(data.responder),
186
+ };
187
+ }
188
+ default:
189
+ return unknown;
190
+ }
191
+ }
192
+ /** `pap_handoff`, as the gateway names it with or without a prefix. */
193
+ export const PAP_HANDOFF_TOOL = /(^|[_.:\-/])pap_handoff$/;
194
+ /**
195
+ * The protocol display of a tool's result, or `null` for one that has none:
196
+ * its embedded display first; else a plain `pap_sign_in`/`pact_sign_in`
197
+ * answer or a `pap_handoff` answer, read by the tool's name.
198
+ */
199
+ export function displayOfToolResult(toolName, result, id = '') {
200
+ const answer = record(result);
201
+ const embedded = record(answer?._meta)?.[DISPLAY_META_KEY];
202
+ if (embedded !== undefined) {
203
+ return protocolDisplayOf(embedded);
204
+ }
205
+ if (isPapSignInTool(toolName)) {
206
+ const read = readPapSignIn(result);
207
+ if (!read)
208
+ return null;
209
+ return 'refused' in read
210
+ ? { kind: 'sign-in-refused', id, why: read.refused }
211
+ : { kind: 'sign-in', id, ...read };
212
+ }
213
+ if (PAP_HANDOFF_TOOL.test(toolName) && answer) {
214
+ const status = text(answer.status);
215
+ if (!status)
216
+ return null;
217
+ return {
218
+ kind: 'handoff',
219
+ id,
220
+ handed: status !== 'closed' && status !== 'direct_conversation_open',
221
+ responder: who(answer.responder),
222
+ note: text(answer.note),
223
+ };
224
+ }
225
+ return null;
226
+ }
227
+ /** What each display says in one line, for a screen reader and a collapsed row. */
228
+ export function displayLine(display) {
229
+ switch (display.kind) {
230
+ case 'sign-in':
231
+ return display.mode === 'mediated'
232
+ ? `Sign in through ${display.host}`
233
+ : `Sign in at ${display.company || display.host}`;
234
+ case 'sign-in-refused':
235
+ return display.why;
236
+ case 'confirm':
237
+ return `${display.company} asks you to confirm: ${display.summary} (version ${display.revision})`;
238
+ case 'outcome':
239
+ return display.state === 'in_progress'
240
+ ? `Still in progress at ${display.company}: ${display.summary}`
241
+ : `${OUTCOME_LEADS[display.state]} ${display.company}: ${display.result}`;
242
+ case 'replies':
243
+ return `${display.company} answered${display.responder === 'human' ? ': one of its people is answering' : ''}`;
244
+ case 'handoff':
245
+ return display.handed
246
+ ? `Handed to ${display.responder === 'human' ? 'one of the company’s people' : 'the company'}`
247
+ : display.note || 'Nothing was handed over.';
248
+ case 'unknown':
249
+ return display.text || `A display this app cannot draw (${display.type})`;
250
+ }
251
+ }
252
+ const OUTCOME_LEADS = {
253
+ succeeded: 'Done at',
254
+ failed: 'Failed at',
255
+ cancelled: 'Cancelled at',
256
+ expired: 'Expired at',
257
+ };
258
+ /** Who wrote a reply, said apart from the person's words and the agent's summaries (PAP-7.8-01). */
259
+ export const replyAuthor = (company, sender) => sender === 'human'
260
+ ? `${company}, one of its people`
261
+ : `${company}, its agent`;
@@ -32,6 +32,7 @@ export function conversationFixture() {
32
32
  status: 'complete',
33
33
  result: 'Shipped on Monday.',
34
34
  error: '',
35
+ display: null,
35
36
  },
36
37
  {
37
38
  kind: 'tool',
@@ -41,6 +42,7 @@ export function conversationFixture() {
41
42
  status: 'error',
42
43
  result: '',
43
44
  error: 'Refused by its rule.',
45
+ display: null,
44
46
  },
45
47
  {
46
48
  kind: 'step',
@@ -13,7 +13,10 @@ export { ASKED_FOR_TAG, askedOf, RUN_END_WORDS } from './records';
13
13
  export { messagesOfThread, openThread, snapshotOfStream, THREAD_WORDS, } from './thread';
14
14
  export { CUT_MARK, DIAGNOSTIC_LIMIT, emptyConversation, formFieldsOf, ITEM_LIMIT, MESSAGE_TEXT_LIMIT, questionOf, questionWords, reduceConversation, TOOL_TEXT_LIMIT, withError, withPrompt, withTurnEnded, type ConversationItem, type ConversationQuestion, type ConversationState, type TurnEnd, } from './conversation';
15
15
  export { PRESENCE_LINES, presenceOfConversation, presenceState, presenceToolOf, type PresenceState, type PresenceTool, } from './presence';
16
- export { ANSWER_BELOW, ANSWER_COMPLETE, composerOf, droppedWords, ENDED_WORDS, messageLabel, OPENING_WORDS, presenceLine, showsQuestion, stepLine, TOOL_STATUS_WORDS, toolLabel, type ComposerProps, type ComposerState, type ConversationViewProps, type ErrorItem, type ErrorRowProps, type MessageItem, type MessageRowProps, type PresenceLineProps, type QuestionCardProps, type StepItem, type StepRowProps, type ToolItem, type ToolRowProps, type TurnEndItem, type TurnEndRowProps, } from './views';
16
+ export { ANSWER_BELOW, ANSWER_COMPLETE, composerOf, droppedWords, ENDED_WORDS, messageLabel, OPENING_WORDS, presenceLine, showsQuestion, stepLine, TOOL_STATUS_WORDS, toolLabel, type ComposerProps, type ComposerState, type ConversationViewProps, type ErrorItem, type ErrorRowProps, type MessageItem, type MessageRowProps, type PresenceLineProps, type ProtocolDisplayProps, type QuestionCardProps, type StepItem, type StepRowProps, type ToolItem, type ToolRowProps, type TurnEndItem, type TurnEndRowProps, } from './views';
17
17
  export { conversationFixture } from './fixtures';
18
+ export { DISPLAY_META_KEY, displayLine, displayOfToolResult, PAP_CONFIRM_TYPE, PAP_HANDOFF_TOOL, PAP_OUTCOME_TYPE, PAP_REPLIES_TYPE, PAP_SIGN_IN_TYPE, protocolDisplayOf, replyAuthor, type ActionClass, type ProtocolDisplay, } from './displays';
19
+ export { isPapSignInTool, PAP_SIGN_IN_WORDS, readPapSignIn, safeSignInUrl, type PapSignInWaiting, } from './signIn';
20
+ export { DIRECT_MAX, DIRECT_WORDS, directAnswerOf, directAuthor, directClient, openDirect, withDirectAnswer, type DirectAnswer, type DirectClient, type DirectClientOptions, type DirectLine, type DirectOpen, type DirectReply, type DirectTarget, } from './direct';
18
21
  export { agentRunClient, agentRuntimesUrlOf, appSessionsClient, RuntimeRefusal, type AgentRunClient, type AgentTurn, type AppSessionsClient, type RuntimeClientOptions, type SessionStart, type StreamOptions, } from './sessions';
19
22
  export type { ChatMessage } from '../types/messages';
@@ -24,5 +24,10 @@ export { PRESENCE_LINES, presenceOfConversation, presenceState, presenceToolOf,
24
24
  // One contract for every renderer of a conversation's components.
25
25
  export { ANSWER_BELOW, ANSWER_COMPLETE, composerOf, droppedWords, ENDED_WORDS, messageLabel, OPENING_WORDS, presenceLine, showsQuestion, stepLine, TOOL_STATUS_WORDS, toolLabel, } from './views';
26
26
  export { conversationFixture } from './fixtures';
27
+ // The protocol displays a run's tool results carry (MOBILE M-037).
28
+ export { DISPLAY_META_KEY, displayLine, displayOfToolResult, PAP_CONFIRM_TYPE, PAP_HANDOFF_TOOL, PAP_OUTCOME_TYPE, PAP_REPLIES_TYPE, PAP_SIGN_IN_TYPE, protocolDisplayOf, replyAuthor, } from './displays';
29
+ export { isPapSignInTool, PAP_SIGN_IN_WORDS, readPapSignIn, safeSignInUrl, } from './signIn';
30
+ // A Direct Conversation the person types in themselves (PAP_DATALAYER P-08, MOBILE M-038).
31
+ export { DIRECT_MAX, DIRECT_WORDS, directAnswerOf, directAuthor, directClient, openDirect, withDirectAnswer, } from './direct';
27
32
  // The runtime's routes a native client drives.
28
33
  export { agentRunClient, agentRuntimesUrlOf, appSessionsClient, RuntimeRefusal, } from './sessions';
@@ -0,0 +1,96 @@
1
+ /**
2
+ * What the gateway's `pap_sign_in` tool answers, read and checked
3
+ * (AGENT_PROTOCOLS PI-56, PAP_DATALAYER P-03): the person signs in at a
4
+ * company themselves — on the company's page (`direct`), by entering a
5
+ * code there (`device`), or by typing what the company asks on Datalayer's
6
+ * own page (`mediated`), never in the conversation. The chat shows the link,
7
+ * the code and until when; the agent never sees what the person types. A
8
+ * business over PACT answers its `pact_sign_in` the same way (a `device`
9
+ * sign-in at the business).
10
+ *
11
+ * Pure: no React. A test reaches it without a chat around it.
12
+ *
13
+ * Kept in `portable/` (MOBILE M-037): Datalayer Mobile reads the same
14
+ * sign-in with the same checks; the web plugin re-exports it.
15
+ *
16
+ * @module portable/signIn
17
+ */
18
+ /** A sign-in waiting for the person, as the card shows it. */
19
+ export type PapSignInWaiting = {
20
+ mode: 'direct' | 'device' | 'mediated';
21
+ /** The company's page — Datalayer's own, for `mediated` — `https` only. */
22
+ open: string;
23
+ /** The page's domain, read from its address: what the person can trust. */
24
+ host: string;
25
+ /**
26
+ * The company's domain, when the gateway names it: for a bound `direct`
27
+ * sign-in the page opened is Datalayer's start link (PAP_DATALAYER P-04),
28
+ * which sends the person on to the company and takes them back only from it.
29
+ */
30
+ company?: string;
31
+ /** The code to enter, for `device`. */
32
+ code?: string;
33
+ /** Where the company sent a code, for `mediated`, when it did. */
34
+ sentTo?: string;
35
+ /** Until when, ISO 8601. */
36
+ expiresAt?: string;
37
+ /**
38
+ * What the company said it needs (its `authorization` event's scope,
39
+ * PAP_DATALAYER P-02): what the person is asked to allow there.
40
+ */
41
+ needs?: string[];
42
+ };
43
+ /** What the card says. */
44
+ export declare const PAP_SIGN_IN_WORDS: {
45
+ readonly title: (host: string) => string;
46
+ readonly direct: "Open the company’s page through Datalayer, sign in there, and this conversation goes on once you are back.";
47
+ readonly device: "Open the company’s page and enter this code there.";
48
+ /** `mediated`: the page is Datalayer's, which names the company and asks what it asks. */
49
+ readonly mediatedTitle: (host: string) => string;
50
+ readonly mediated: "Open this page: it names the company and asks what the company needs. Type it there, never in this conversation.";
51
+ readonly sentTo: (to: string) => string;
52
+ readonly needs: (scopes: string[]) => string;
53
+ readonly open: (host: string) => string;
54
+ readonly code: "Your code";
55
+ readonly copy: "Copy the code";
56
+ readonly copied: "Copied";
57
+ readonly until: (at: string) => string;
58
+ readonly checkAgain: "I have signed in: check again";
59
+ readonly tellMe: "Tell me when you are done.";
60
+ readonly checkMessage: (host: string) => string;
61
+ readonly refused: "This sign-in link cannot be opened safely, so it is not shown.";
62
+ readonly qr: (host: string) => string;
63
+ /** Once the person's browser came back from the company (P-04), said under the card. */
64
+ readonly returned: {
65
+ readonly signed_in: (company: string) => string;
66
+ readonly denied: (company: string) => string;
67
+ readonly refused: (company: string) => string;
68
+ };
69
+ };
70
+ /**
71
+ * Where Datalayer's `/pap/signed-in` page says a Direct Sign-In came back
72
+ * (PAP_DATALAYER P-04): the card waiting for that company hears it in the
73
+ * person's tab, so the conversation goes on there.
74
+ */
75
+ export declare const PAP_SIGN_IN_CHANNEL = "datalayer-pap-sign-in";
76
+ /** How a Direct Sign-In came back. */
77
+ export type PapSignInReturn = keyof typeof PAP_SIGN_IN_WORDS.returned;
78
+ /** What a message on the channel says of this company's sign-in, or `null`: another company's, or not one. */
79
+ export declare function signInReturnOf(message: unknown, company: string): PapSignInReturn | null;
80
+ /**
81
+ * `pap_sign_in` — or a business's `pact_sign_in` (PI-56) — as the gateway
82
+ * names it with or without a prefix.
83
+ */
84
+ export declare const PAP_SIGN_IN_TOOL: RegExp;
85
+ /** Whether a tool is `pap_sign_in` or `pact_sign_in`, as the gateway names it with or without a prefix. */
86
+ export declare const isPapSignInTool: (name: string) => boolean;
87
+ /** The page's address, `https` only; `null` for anything else. */
88
+ export declare function safeSignInUrl(open: unknown): URL | null;
89
+ /**
90
+ * The sign-in waiting for the person, or why it is not shown: `null` for a
91
+ * result that is not one (another status: the chat's default row says it),
92
+ * `{ refused }` for a link that is not `https`.
93
+ */
94
+ export declare function readPapSignIn(result: unknown): PapSignInWaiting | {
95
+ refused: string;
96
+ } | null;
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** What the card says. */
6
+ export const PAP_SIGN_IN_WORDS = {
7
+ title: (host) => `Sign in at ${host}`,
8
+ direct: 'Open the company’s page through Datalayer, sign in there, and this conversation goes on once you are back.',
9
+ device: 'Open the company’s page and enter this code there.',
10
+ /** `mediated`: the page is Datalayer's, which names the company and asks what it asks. */
11
+ mediatedTitle: (host) => `Sign in through ${host}`,
12
+ mediated: 'Open this page: it names the company and asks what the company needs. Type it there, never in this conversation.',
13
+ sentTo: (to) => `The company sent a code to ${to}.`,
14
+ needs: (scopes) => `It needs: ${scopes.join(', ')}.`,
15
+ open: (host) => `Open ${host}`,
16
+ code: 'Your code',
17
+ copy: 'Copy the code',
18
+ copied: 'Copied',
19
+ until: (at) => `Until ${new Date(at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}.`,
20
+ checkAgain: 'I have signed in: check again',
21
+ tellMe: 'Tell me when you are done.',
22
+ checkMessage: (host) => `I have signed in at ${host}. Please check again.`,
23
+ refused: 'This sign-in link cannot be opened safely, so it is not shown.',
24
+ qr: (host) => `A code to scan to open ${host} on your phone`,
25
+ /** Once the person's browser came back from the company (P-04), said under the card. */
26
+ returned: {
27
+ signed_in: (company) => `Signed in at ${company}: checking again.`,
28
+ denied: (company) => `You did not sign in at ${company}: ask again when you want to.`,
29
+ refused: (company) => `That sign-in at ${company} was refused, and nothing was kept: start it again here.`,
30
+ },
31
+ };
32
+ /**
33
+ * Where Datalayer's `/pap/signed-in` page says a Direct Sign-In came back
34
+ * (PAP_DATALAYER P-04): the card waiting for that company hears it in the
35
+ * person's tab, so the conversation goes on there.
36
+ */
37
+ export const PAP_SIGN_IN_CHANNEL = 'datalayer-pap-sign-in';
38
+ /** One OAuth scope token (RFC 6749 §3.3). */
39
+ const SCOPE = /^[\x21\x23-\x5B\x5D-\x7E]{1,128}$/;
40
+ const DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
41
+ /** What a message on the channel says of this company's sign-in, or `null`: another company's, or not one. */
42
+ export function signInReturnOf(message, company) {
43
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
44
+ return null;
45
+ }
46
+ const said = message;
47
+ const outcome = said.outcome;
48
+ return said.company === company &&
49
+ (outcome === 'signed_in' || outcome === 'denied' || outcome === 'refused')
50
+ ? outcome
51
+ : null;
52
+ }
53
+ /**
54
+ * `pap_sign_in` — or a business's `pact_sign_in` (PI-56) — as the gateway
55
+ * names it with or without a prefix.
56
+ */
57
+ export const PAP_SIGN_IN_TOOL = /(^|[_.:\-/])pa(?:p|ct)_sign_in$/;
58
+ /** Whether a tool is `pap_sign_in` or `pact_sign_in`, as the gateway names it with or without a prefix. */
59
+ export const isPapSignInTool = (name) => PAP_SIGN_IN_TOOL.test(name);
60
+ /** An MCP result's own object: its structured content, or the JSON of its text. */
61
+ function unwrapped(result) {
62
+ let value = result;
63
+ if (typeof value === 'string') {
64
+ try {
65
+ value = JSON.parse(value);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
72
+ return null;
73
+ }
74
+ const data = value;
75
+ if (data.structuredContent && typeof data.structuredContent === 'object') {
76
+ return unwrapped(data.structuredContent);
77
+ }
78
+ if (Array.isArray(data.content)) {
79
+ const text = data.content.find(part => part?.type === 'text' && typeof part.text === 'string');
80
+ return text ? unwrapped(text.text) : null;
81
+ }
82
+ // FastMCP wraps a non-object answer as `{result: …}`.
83
+ if (data.result && typeof data.result === 'object' && !('status' in data)) {
84
+ return unwrapped(data.result);
85
+ }
86
+ return data;
87
+ }
88
+ /** The page's address, `https` only; `null` for anything else. */
89
+ export function safeSignInUrl(open) {
90
+ if (typeof open !== 'string') {
91
+ return null;
92
+ }
93
+ try {
94
+ const url = new URL(open);
95
+ return url.protocol === 'https:' && url.hostname ? url : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * The sign-in waiting for the person, or why it is not shown: `null` for a
103
+ * result that is not one (another status: the chat's default row says it),
104
+ * `{ refused }` for a link that is not `https`.
105
+ */
106
+ export function readPapSignIn(result) {
107
+ const data = unwrapped(result);
108
+ if (!data || data.status !== 'waiting_for_the_person') {
109
+ return null;
110
+ }
111
+ if (data.mode !== 'direct' &&
112
+ data.mode !== 'device' &&
113
+ data.mode !== 'mediated') {
114
+ return null;
115
+ }
116
+ const url = safeSignInUrl(data.open);
117
+ if (!url) {
118
+ return { refused: PAP_SIGN_IN_WORDS.refused };
119
+ }
120
+ if (data.mode === 'device' && (typeof data.code !== 'string' || !data.code)) {
121
+ return null;
122
+ }
123
+ // A bound Direct Sign-In opens Datalayer's start link: the card names the company the gateway
124
+ // names (`PapSignIn.company`); without one, the page's own domain is the company's.
125
+ const company = typeof data.company === 'string' && DOMAIN.test(data.company)
126
+ ? data.company
127
+ : '';
128
+ // What the company said it needs: scope tokens only, anything else not shown.
129
+ const needs = Array.isArray(data.needs)
130
+ ? data.needs.filter((scope) => typeof scope === 'string' && SCOPE.test(scope))
131
+ : [];
132
+ return {
133
+ mode: data.mode,
134
+ open: url.toString(),
135
+ host: url.hostname,
136
+ ...(company ? { company } : {}),
137
+ ...(typeof data.code === 'string' && data.code ? { code: data.code } : {}),
138
+ ...(data.mode === 'mediated' &&
139
+ typeof data.sent_to === 'string' &&
140
+ data.sent_to
141
+ ? { sentTo: data.sent_to }
142
+ : {}),
143
+ ...(typeof data.expires_at === 'string' && data.expires_at
144
+ ? { expiresAt: data.expires_at }
145
+ : {}),
146
+ ...(needs.length > 0 ? { needs } : {}),
147
+ };
148
+ }