@datalayer/agent-runtimes 1.5.24 → 1.5.26

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,186 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** What IAM keeps of one field, at most. */
6
+ export const MAX_VALUE_LENGTH = 1024;
7
+ /** What the page says (checked against the vocabulary). */
8
+ export const PROTECTED_INPUT_WORDS = {
9
+ title: (company) => `Sign in at ${company}`,
10
+ intro: (company) => `Your personal agent is signing you in at ${company}, which asks for what only you know. Type it here: Datalayer passes it to ${company} once, and your agent never sees it.`,
11
+ asks: 'Who asks',
12
+ where: 'Where',
13
+ whereHint: 'The company’s address: the part you can check.',
14
+ sentCode: (company, sentTo) => `${company} sent a code to ${sentTo}.`,
15
+ sentCodeSomewhere: (company) => `${company} sent you a code.`,
16
+ code: 'Code',
17
+ signIn: (company) => `Sign in at ${company}`,
18
+ sendCode: 'Send the code',
19
+ sending: 'Sending…',
20
+ fillEvery: 'Fill in every field.',
21
+ tooLong: (label) => `${label} is longer than ${MAX_VALUE_LENGTH} characters.`,
22
+ done: 'Done. Go back to your conversation: the agent continues once it checks.',
23
+ expires: (at) => `This request ends at ${new Date(at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}.`,
24
+ reading: 'Reading the request…',
25
+ gone: 'This request is unknown, already answered or expired. Ask your personal agent to start again.',
26
+ incomplete: 'This link is incomplete: it names no request. Start again from your personal agent.',
27
+ signedOut: 'Sign in to Datalayer first.',
28
+ };
29
+ const TICKET = /^[A-Za-z0-9_-]{8,256}$/;
30
+ const FIELD_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
31
+ /** Whether a link names a request. */
32
+ export const ticketComplete = (ticket) => TICKET.test(ticket.trim());
33
+ /** Raised when IAM does not know the request (unknown, answered or expired): the page says only that. */
34
+ export class ProtectedRequestGone extends Error {
35
+ constructor() {
36
+ super(PROTECTED_INPUT_WORDS.gone);
37
+ this.name = 'ProtectedRequestGone';
38
+ }
39
+ }
40
+ /** IAM's answer read whole, or refused with a sentence: no default. */
41
+ export function protectedRequestOf(raw) {
42
+ const refused = () => new Error('The request could not be read.');
43
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
44
+ throw refused();
45
+ }
46
+ const data = raw;
47
+ if (typeof data.company_name !== 'string' ||
48
+ typeof data.company_domain !== 'string' ||
49
+ !data.company_domain ||
50
+ (data.purpose !== 'credentials' && data.purpose !== 'one_time_code') ||
51
+ !Array.isArray(data.fields) ||
52
+ data.fields.length === 0 ||
53
+ data.fields.length > 10 ||
54
+ typeof data.expires_at !== 'string' ||
55
+ !(data.sent_to === null ||
56
+ data.sent_to === undefined ||
57
+ typeof data.sent_to === 'string')) {
58
+ throw refused();
59
+ }
60
+ const names = new Set();
61
+ const fields = data.fields.map(item => {
62
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
63
+ throw refused();
64
+ }
65
+ const entry = item;
66
+ if (typeof entry.name !== 'string' ||
67
+ !FIELD_NAME.test(entry.name) ||
68
+ names.has(entry.name) ||
69
+ typeof entry.label !== 'string' ||
70
+ typeof entry.secret !== 'boolean') {
71
+ throw refused();
72
+ }
73
+ names.add(entry.name);
74
+ return {
75
+ name: entry.name,
76
+ label: entry.label || entry.name,
77
+ secret: entry.secret,
78
+ };
79
+ });
80
+ if (data.purpose === 'one_time_code' && fields.length !== 1) {
81
+ throw refused();
82
+ }
83
+ return {
84
+ companyName: data.company_name || data.company_domain,
85
+ companyDomain: data.company_domain,
86
+ purpose: data.purpose,
87
+ fields,
88
+ sentTo: typeof data.sent_to === 'string' && data.sent_to ? data.sent_to : null,
89
+ expiresAt: data.expires_at,
90
+ };
91
+ }
92
+ /** The fields still empty, by name: the form is sent only when there is none. */
93
+ export const emptyFields = (request, values) => request.fields
94
+ .filter(field => !(values[field.name] ?? '').trim())
95
+ .map(field => field.name);
96
+ /** What keeps the form from being sent, in sentences; none when it can be. Never what was typed. */
97
+ export function fieldProblems(request, values) {
98
+ const problems = [];
99
+ if (emptyFields(request, values).length) {
100
+ problems.push(PROTECTED_INPUT_WORDS.fillEvery);
101
+ }
102
+ for (const field of request.fields) {
103
+ if ((values[field.name] ?? '').length > MAX_VALUE_LENGTH) {
104
+ problems.push(PROTECTED_INPUT_WORDS.tooLong(field.label));
105
+ }
106
+ }
107
+ return problems;
108
+ }
109
+ /** The words on the one button. */
110
+ export const submitWords = (request) => request.purpose === 'one_time_code'
111
+ ? PROTECTED_INPUT_WORDS.sendCode
112
+ : PROTECTED_INPUT_WORDS.signIn(request.companyName);
113
+ /** What the code page says of where the code went. */
114
+ export const sentWords = (request) => request.sentTo
115
+ ? PROTECTED_INPUT_WORDS.sentCode(request.companyName, request.sentTo)
116
+ : PROTECTED_INPUT_WORDS.sentCodeSomewhere(request.companyName);
117
+ /**
118
+ * The body sent: exactly the fields asked, none empty, as typed. A code is
119
+ * sent without its spaces; a password, exactly as typed.
120
+ */
121
+ export function submitBody(request, values) {
122
+ const problems = fieldProblems(request, values);
123
+ if (problems.length) {
124
+ throw new Error(problems.join(' '));
125
+ }
126
+ const sent = {};
127
+ for (const field of request.fields) {
128
+ const typed = values[field.name];
129
+ sent[field.name] =
130
+ request.purpose === 'one_time_code' ? typed.replace(/\s+/g, '') : typed;
131
+ }
132
+ return { values: sent };
133
+ }
134
+ /** The ticket a mediated sign-in page names (`…?ticket=`), or `''`. */
135
+ export function ticketOfPage(open) {
136
+ try {
137
+ const ticket = new URL(open).searchParams.get('ticket') ?? '';
138
+ return ticketComplete(ticket) ? ticket.trim() : '';
139
+ }
140
+ catch {
141
+ return '';
142
+ }
143
+ }
144
+ /** IAM's protected-input routes, with the person's own sign-in. */
145
+ export function protectedInputClient({ iamUrl, getAccessToken, fetch: fetcher, }) {
146
+ const address = (ticket) => {
147
+ if (!iamUrl) {
148
+ throw new Error('Datalayer’s sign-in service is not configured.');
149
+ }
150
+ return `${iamUrl.replace(/\/+$/, '')}/api/iam/v1/pap/my/protected/${encodeURIComponent(ticket.trim())}`;
151
+ };
152
+ const headers = () => {
153
+ const token = getAccessToken();
154
+ return {
155
+ 'Content-Type': 'application/json',
156
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
157
+ };
158
+ };
159
+ /** IAM's refusal in its own sentence; never what was typed. */
160
+ const answered = async (response, what) => {
161
+ const body = (await response.json().catch(() => ({})));
162
+ if (response.status === 404) {
163
+ throw new ProtectedRequestGone();
164
+ }
165
+ if (!response.ok) {
166
+ throw new Error(`${what}: ${typeof body.detail === 'string' ? body.detail : response.status}`);
167
+ }
168
+ return body;
169
+ };
170
+ return {
171
+ /** What the company asks; reading it does not use the request up. */
172
+ read: async (ticket) => protectedRequestOf(await answered(await fetcher(address(ticket), { headers: headers() }), 'The request could not be read')),
173
+ /** What the person typed, sent once to IAM. */
174
+ send: async (ticket, request, values) => {
175
+ const body = submitBody(request, values);
176
+ const answer = await answered(await fetcher(address(ticket), {
177
+ method: 'POST',
178
+ headers: headers(),
179
+ body: JSON.stringify(body),
180
+ }), 'It was not sent');
181
+ if (answer.kept !== true) {
182
+ throw new Error('It was not sent: the answer does not say it was kept.');
183
+ }
184
+ },
185
+ };
186
+ }
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * How a sign-in under way stands, asked by the person's own client (MOBILE
3
+ * M-039): the gateway's `GET /api/mcp/v1/pap/sign-in?app_uid&company`, with
4
+ * the person's own sign-in, finishes what can be finished — a Direct return
5
+ * IAM bound to their browser and the company asked, a Device code the
6
+ * company confirmed — and says where it stands.
7
+ *
8
+ * A continuation is accepted only when it is bound: the answer names the
9
+ * application and the company the card was for, and a verified issuer. The
10
+ * client then goes on in the same conversation (its session), once.
11
+ *
12
+ * Pure but for the given `fetch`.
13
+ *
14
+ * @module portable/signInStatus
15
+ */
16
+ /** Where the gateway answers how a sign-in stands. */
17
+ export declare const SIGN_IN_STATUS_PATH = "/api/mcp/v1/pap/sign-in";
18
+ /** How often a client asks while a sign-in waits, and never past its expiry. */
19
+ export declare const SIGN_IN_POLL_MS = 5000;
20
+ export type SignInStands = {
21
+ appUid: string;
22
+ company: string;
23
+ issuer: string;
24
+ status: 'pending' | 'signed_in' | 'refused' | 'denied' | 'not_started' | 'other';
25
+ reason: string;
26
+ };
27
+ /** The gateway's answer, or `null` when it does not bind to this application and company. */
28
+ export declare function signInStandsOf(raw: unknown, expected: {
29
+ appUid: string;
30
+ company: string;
31
+ }): SignInStands | null;
32
+ /** Asks how a sign-in stands; `null` for none under way (404) or an answer that does not bind. */
33
+ export declare function signInStatusClient({ baseUrl, getAccessToken, fetch: fetcher, }: {
34
+ baseUrl: string;
35
+ getAccessToken: () => string | undefined;
36
+ fetch: typeof fetch;
37
+ }): (appUid: string, company: string) => Promise<SignInStands | null>;
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * How a sign-in under way stands, asked by the person's own client (MOBILE
7
+ * M-039): the gateway's `GET /api/mcp/v1/pap/sign-in?app_uid&company`, with
8
+ * the person's own sign-in, finishes what can be finished — a Direct return
9
+ * IAM bound to their browser and the company asked, a Device code the
10
+ * company confirmed — and says where it stands.
11
+ *
12
+ * A continuation is accepted only when it is bound: the answer names the
13
+ * application and the company the card was for, and a verified issuer. The
14
+ * client then goes on in the same conversation (its session), once.
15
+ *
16
+ * Pure but for the given `fetch`.
17
+ *
18
+ * @module portable/signInStatus
19
+ */
20
+ /** Where the gateway answers how a sign-in stands. */
21
+ export const SIGN_IN_STATUS_PATH = '/api/mcp/v1/pap/sign-in';
22
+ /** How often a client asks while a sign-in waits, and never past its expiry. */
23
+ export const SIGN_IN_POLL_MS = 5000;
24
+ const STATUSES = [
25
+ 'pending',
26
+ 'signed_in',
27
+ 'refused',
28
+ 'denied',
29
+ 'not_started',
30
+ ];
31
+ /** The gateway's answer, or `null` when it does not bind to this application and company. */
32
+ export function signInStandsOf(raw, expected) {
33
+ const data = raw && typeof raw === 'object' && !Array.isArray(raw)
34
+ ? raw
35
+ : null;
36
+ if (!data ||
37
+ data.app_uid !== expected.appUid ||
38
+ typeof data.company !== 'string' ||
39
+ data.company.toLowerCase() !== expected.company.toLowerCase() ||
40
+ typeof data.issuer !== 'string' ||
41
+ !data.issuer.startsWith('https://')) {
42
+ return null;
43
+ }
44
+ return {
45
+ appUid: expected.appUid,
46
+ company: data.company,
47
+ issuer: data.issuer,
48
+ status: STATUSES.find(status => status === data.status) ?? 'other',
49
+ reason: typeof data.reason === 'string' ? data.reason : '',
50
+ };
51
+ }
52
+ /** Asks how a sign-in stands; `null` for none under way (404) or an answer that does not bind. */
53
+ export function signInStatusClient({ baseUrl, getAccessToken, fetch: fetcher, }) {
54
+ return async (appUid, company) => {
55
+ const token = getAccessToken();
56
+ if (!token) {
57
+ return null;
58
+ }
59
+ const query = new URLSearchParams({ app_uid: appUid, company });
60
+ const response = await fetcher(`${baseUrl.replace(/\/+$/, '')}${SIGN_IN_STATUS_PATH}?${query.toString()}`, {
61
+ headers: {
62
+ Authorization: `Bearer ${token}`,
63
+ Accept: 'application/json',
64
+ },
65
+ });
66
+ if (response.status === 404) {
67
+ return null;
68
+ }
69
+ if (!response.ok) {
70
+ throw new Error(`How the sign-in at ${company} stands could not be read (${response.status}).`);
71
+ }
72
+ return signInStandsOf(await response.json(), { appUid, company });
73
+ };
74
+ }
@@ -11,6 +11,7 @@
11
11
  * @module portable/views
12
12
  */
13
13
  import type { ConversationItem, ConversationQuestion, ConversationState } from './conversation';
14
+ import type { ProtocolDisplay } from './displays';
14
15
  import { type PresenceState } from './presence';
15
16
  export type MessageItem = Extract<ConversationItem, {
16
17
  kind: 'message';
@@ -58,6 +59,27 @@ export type QuestionCardProps = {
58
59
  /** A form, answered by a settings change. */
59
60
  onForm: (values: Record<string, unknown>) => void;
60
61
  };
62
+ /**
63
+ * A protocol display under its tool row (MOBILE M-037): what it shows is
64
+ * `displayLine`'s and the display's own; what the person does goes out
65
+ * through the host — a sign-in page opened outside the conversation, an
66
+ * operation answered where approvals are, *I have signed in* said as their
67
+ * next message. Nothing is answered from the card itself.
68
+ */
69
+ export type ProtocolDisplayProps = {
70
+ display: ProtocolDisplay;
71
+ /** Open a sign-in page, `https` only, outside the conversation. */
72
+ onOpenSignIn: (url: string) => void;
73
+ /** Open the approval an operation stands for. */
74
+ onOpenApproval: (approvalId: string) => void;
75
+ /** The person says they signed in (`PAP_SIGN_IN_WORDS.checkMessage`). */
76
+ onSignedIn: (words: string) => void;
77
+ /**
78
+ * Open a Direct Conversation under the conversation a company answered in
79
+ * (`portable/direct`, MOBILE M-038); absent where none can be opened.
80
+ */
81
+ onOpenDirect?: (company: string, conversationId: string) => void;
82
+ };
61
83
  /** The line beside the application's face. */
62
84
  export type PresenceLineProps = {
63
85
  state: PresenceState;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Accounting",
3
3
  "description": "Answers requests for financial reports, such as open invoices, aged balances, a trial balance or a customer's ledger, from the Odoo books, which it only reads.",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "supportedInterfaces": [
6
6
  {
7
7
  "protocolBinding": "JSONRPC",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.5.24",
3
+ "version": "1.5.26",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",