@curless/hotel-storefront 0.24.4 → 0.26.0
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/__tests__/__helpers__/sessions.d.ts +10 -0
- package/dist/__tests__/__helpers__/sessions.d.ts.map +1 -0
- package/dist/__tests__/__helpers__/sessions.js +37 -0
- package/dist/__tests__/__helpers__/sessions.js.map +1 -0
- package/dist/__tests__/card-contract.test.js +20 -0
- package/dist/__tests__/card-contract.test.js.map +1 -1
- package/dist/__tests__/data.test.js +7 -1
- package/dist/__tests__/data.test.js.map +1 -1
- package/dist/__tests__/dates.test.js +13 -1
- package/dist/__tests__/dates.test.js.map +1 -1
- package/dist/__tests__/huazhu-audit.test.js +69 -4
- package/dist/__tests__/huazhu-audit.test.js.map +1 -1
- package/dist/__tests__/huazhu-backend.test.js +93 -0
- package/dist/__tests__/huazhu-backend.test.js.map +1 -1
- package/dist/__tests__/huazhu-sandbox.test.js +7 -1
- package/dist/__tests__/huazhu-sandbox.test.js.map +1 -1
- package/dist/__tests__/server.test.js +112 -3
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/state.test.d.ts +2 -0
- package/dist/__tests__/state.test.d.ts.map +1 -0
- package/dist/__tests__/state.test.js +204 -0
- package/dist/__tests__/state.test.js.map +1 -0
- package/dist/__tests__/tools.test.js +18 -0
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/backend.d.ts +5 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -11
- package/dist/config.js.map +1 -1
- package/dist/dates.d.ts +9 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +32 -0
- package/dist/dates.js.map +1 -1
- package/dist/demo-hotels.d.ts +21 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +83 -11
- package/dist/demo-hotels.js.map +1 -1
- package/dist/huazhu/backend.d.ts +5 -0
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +67 -9
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/legal.d.ts +8 -1
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +22 -8
- package/dist/legal.js.map +1 -1
- package/dist/limits.d.ts +49 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +45 -0
- package/dist/limits.js.map +1 -1
- package/dist/members.d.ts +3 -1
- package/dist/members.d.ts.map +1 -1
- package/dist/members.js +24 -6
- package/dist/members.js.map +1 -1
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +8 -1
- package/dist/money.js.map +1 -1
- package/dist/server.d.ts +22 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +121 -37
- package/dist/server.js.map +1 -1
- package/dist/sessions.d.ts +19 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +96 -0
- package/dist/sessions.js.map +1 -0
- package/dist/state.d.ts +32 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +118 -0
- package/dist/state.js.map +1 -0
- package/dist/tools.d.ts +13 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +69 -5
- package/dist/tools.js.map +1 -1
- package/dist/trip-ids.d.ts +8 -1
- package/dist/trip-ids.d.ts.map +1 -1
- package/dist/trip-ids.js +24 -1
- package/dist/trip-ids.js.map +1 -1
- package/dist/trip.d.ts +3 -1
- package/dist/trip.d.ts.map +1 -1
- package/dist/trip.js +23 -6
- package/dist/trip.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
// The session ids this connector has issued - and only those are accepted.
|
|
3
|
+
//
|
|
4
|
+
// ⚠️ A SESSION ID WAS ANY STRING THE CALLER SENT. `initialize` issued one, but
|
|
5
|
+
// every other call took whatever `Mcp-Session-Id` arrived, so a script could
|
|
6
|
+
// be as many conversations as it liked: twelve made-up ids, one booking each,
|
|
7
|
+
// emptied the Island Shangri-La for a month (2026-10-05 audit). Now an id this
|
|
8
|
+
// connector never issued - or issued and has since forgotten - is answered
|
|
9
|
+
// 404, which the protocol defines as "this session is gone, initialize again".
|
|
10
|
+
//
|
|
11
|
+
// This alone is not the fix for that attack (`initialize` hands out ids for
|
|
12
|
+
// the asking); the demo-wide cap on held rooms is. What it does guarantee is
|
|
13
|
+
// that a conversation's id is one we chose.
|
|
14
|
+
//
|
|
15
|
+
// ⚠️ KEPT ACROSS A RESTART, or every deploy would answer every open
|
|
16
|
+
// conversation 404 - and a host that does not start over on a 404 shows its
|
|
17
|
+
// guest an error instead. A call with NO session id is still answered: the
|
|
18
|
+
// card's calls can arrive without one (trip-ids.ts).
|
|
19
|
+
/** An id unused this long is forgotten; a conversation picked up after that starts a new session. */
|
|
20
|
+
export const SESSION_TTL_MS = 30 * 86_400_000;
|
|
21
|
+
/** Ids kept at once. Past it, the one unused for longest goes. */
|
|
22
|
+
const MAX_SESSIONS = 100_000;
|
|
23
|
+
/**
|
|
24
|
+
* How stale a "last used" may get before it is written down again. A session
|
|
25
|
+
* lives for a month; rewriting the file on every tool call to move its clock by
|
|
26
|
+
* a second would be most of what this server writes.
|
|
27
|
+
*/
|
|
28
|
+
const TOUCH_EVERY_MS = 60 * 60_000;
|
|
29
|
+
export class Sessions {
|
|
30
|
+
now;
|
|
31
|
+
file;
|
|
32
|
+
/** Id → last used. Insertion order is use order: a touch re-inserts. */
|
|
33
|
+
seen = new Map();
|
|
34
|
+
sweptAt = 0;
|
|
35
|
+
constructor(now = () => Date.now(), file) {
|
|
36
|
+
this.now = now;
|
|
37
|
+
this.file = file;
|
|
38
|
+
const saved = file?.read();
|
|
39
|
+
for (const [id, at] of saved?.sessions ?? []) {
|
|
40
|
+
if (typeof id === 'string' && typeof at === 'number')
|
|
41
|
+
this.seen.set(id, at);
|
|
42
|
+
}
|
|
43
|
+
file?.bind(() => ({ sessions: [...this.seen] }));
|
|
44
|
+
this.sweep();
|
|
45
|
+
}
|
|
46
|
+
/** A new id, ours. */
|
|
47
|
+
issue() {
|
|
48
|
+
this.sweep();
|
|
49
|
+
const id = randomUUID();
|
|
50
|
+
this.seen.set(id, this.now());
|
|
51
|
+
while (this.seen.size > MAX_SESSIONS) {
|
|
52
|
+
const oldest = this.seen.keys().next().value;
|
|
53
|
+
if (oldest === undefined)
|
|
54
|
+
break;
|
|
55
|
+
this.seen.delete(oldest);
|
|
56
|
+
}
|
|
57
|
+
this.file?.changed();
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
/** Whether this is an id we issued and still keep - and, if so, that it was just used. */
|
|
61
|
+
known(id) {
|
|
62
|
+
const at = this.seen.get(id);
|
|
63
|
+
if (at === undefined)
|
|
64
|
+
return false;
|
|
65
|
+
const t = this.now();
|
|
66
|
+
if (t - at > SESSION_TTL_MS) {
|
|
67
|
+
this.seen.delete(id);
|
|
68
|
+
this.file?.changed();
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (t - at > TOUCH_EVERY_MS) {
|
|
72
|
+
this.seen.delete(id);
|
|
73
|
+
this.seen.set(id, t);
|
|
74
|
+
this.file?.changed();
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
get size() {
|
|
79
|
+
return this.seen.size;
|
|
80
|
+
}
|
|
81
|
+
/** At most once an hour: a walk over a hundred thousand ids is not free. */
|
|
82
|
+
sweep() {
|
|
83
|
+
const t = this.now();
|
|
84
|
+
if (t - this.sweptAt < TOUCH_EVERY_MS)
|
|
85
|
+
return;
|
|
86
|
+
this.sweptAt = t;
|
|
87
|
+
let gone = false;
|
|
88
|
+
for (const [id, at] of this.seen) {
|
|
89
|
+
if (t - at > SESSION_TTL_MS)
|
|
90
|
+
gone = this.seen.delete(id) || gone;
|
|
91
|
+
}
|
|
92
|
+
if (gone)
|
|
93
|
+
this.file?.changed();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare class StateFile {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
private timer;
|
|
4
|
+
private snapshot;
|
|
5
|
+
constructor(path: string);
|
|
6
|
+
/**
|
|
7
|
+
* What was written last time, or undefined when there is nothing usable.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ A FILE THAT DOES NOT PARSE IS SET ASIDE, NOT OVERWRITTEN. Starting
|
|
10
|
+
* empty and writing over it would destroy the only copy of somebody's
|
|
11
|
+
* booking; renamed, a person can still look at it.
|
|
12
|
+
*/
|
|
13
|
+
read<T>(): T | undefined;
|
|
14
|
+
/** What to write. The store hands over a function, so the write takes the state as it is THEN. */
|
|
15
|
+
bind(snapshot: () => unknown): void;
|
|
16
|
+
/** Something changed: write it soon. */
|
|
17
|
+
changed(): void;
|
|
18
|
+
/** Write now if anything is waiting. */
|
|
19
|
+
flush(): void;
|
|
20
|
+
private write;
|
|
21
|
+
}
|
|
22
|
+
/** Where the stores of one deployment keep their files - or nothing, when it has no directory. */
|
|
23
|
+
export declare class StateDir {
|
|
24
|
+
readonly dir: string | undefined;
|
|
25
|
+
private readonly files;
|
|
26
|
+
constructor(dir: string | undefined);
|
|
27
|
+
get persisted(): boolean;
|
|
28
|
+
file(name: string): StateFile | undefined;
|
|
29
|
+
/** Everything still waiting, written now - on a clean stop. */
|
|
30
|
+
flush(): void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAuBA,qBAAa,SAAS;IAIR,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHjC,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,QAAQ,CAA8B;gBAEzB,IAAI,EAAE,MAAM;IAEjC;;;;;;OAMG;IACH,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,SAAS;IAuBxB,kGAAkG;IAClG,IAAI,CAAC,QAAQ,EAAE,MAAM,OAAO,GAAG,IAAI;IAInC,wCAAwC;IACxC,OAAO,IAAI,IAAI;IAMf,wCAAwC;IACxC,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,KAAK;CAed;AAED,kGAAkG;AAClG,qBAAa,QAAQ;IAGP,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAF5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;gBAEpB,GAAG,EAAE,MAAM,GAAG,SAAS;IAE5C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAOzC,+DAA+D;IAC/D,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
// What survives a restart: one JSON file per store, in the deployment's state
|
|
4
|
+
// directory (`HOTEL_STATE_DIR`).
|
|
5
|
+
//
|
|
6
|
+
// ⚠️ A DEPLOY USED TO FORGET EVERY GUEST. Bookings, trips, sign-ins and the
|
|
7
|
+
// session ids themselves were in memory, and this connector ships several times
|
|
8
|
+
// a day: a guest told "held until 18:00, SL-7K2M4P" asked to cancel after a
|
|
9
|
+
// deploy and was told no booking matched - and the lookup with their email was
|
|
10
|
+
// then counted as a wrong guess (2026-10-05 audit). Now every store writes
|
|
11
|
+
// itself here, and reads itself back on boot.
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ NO DIRECTORY, NO DISK. Without one the stores stay in memory exactly as
|
|
14
|
+
// before, so a local run or a test writes nothing anywhere. And the directory
|
|
15
|
+
// is never the brand's repository: that is mounted read-only on the box.
|
|
16
|
+
//
|
|
17
|
+
// Writes are coalesced - a trip id touched on every tool call would otherwise
|
|
18
|
+
// rewrite its file on every tool call - so the last half second before a crash
|
|
19
|
+
// can be lost. A clean stop (`flush`, on SIGTERM) loses nothing.
|
|
20
|
+
const WRITE_DELAY_MS = 500;
|
|
21
|
+
export class StateFile {
|
|
22
|
+
path;
|
|
23
|
+
timer;
|
|
24
|
+
snapshot;
|
|
25
|
+
constructor(path) {
|
|
26
|
+
this.path = path;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What was written last time, or undefined when there is nothing usable.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ A FILE THAT DOES NOT PARSE IS SET ASIDE, NOT OVERWRITTEN. Starting
|
|
32
|
+
* empty and writing over it would destroy the only copy of somebody's
|
|
33
|
+
* booking; renamed, a person can still look at it.
|
|
34
|
+
*/
|
|
35
|
+
read() {
|
|
36
|
+
let text;
|
|
37
|
+
try {
|
|
38
|
+
text = readFileSync(this.path, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined; // none yet
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const aside = `${this.path}.corrupt-${Date.now()}`;
|
|
48
|
+
try {
|
|
49
|
+
renameSync(this.path, aside);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Unreadable and unmovable: starting empty is all that is left.
|
|
53
|
+
}
|
|
54
|
+
console.error(`[state] ${this.path} did not parse (${err.message}); kept as ${aside}`);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** What to write. The store hands over a function, so the write takes the state as it is THEN. */
|
|
59
|
+
bind(snapshot) {
|
|
60
|
+
this.snapshot = snapshot;
|
|
61
|
+
}
|
|
62
|
+
/** Something changed: write it soon. */
|
|
63
|
+
changed() {
|
|
64
|
+
if (this.timer)
|
|
65
|
+
return;
|
|
66
|
+
this.timer = setTimeout(() => this.flush(), WRITE_DELAY_MS);
|
|
67
|
+
this.timer.unref?.();
|
|
68
|
+
}
|
|
69
|
+
/** Write now if anything is waiting. */
|
|
70
|
+
flush() {
|
|
71
|
+
if (!this.timer)
|
|
72
|
+
return;
|
|
73
|
+
clearTimeout(this.timer);
|
|
74
|
+
this.timer = undefined;
|
|
75
|
+
this.write();
|
|
76
|
+
}
|
|
77
|
+
write() {
|
|
78
|
+
if (!this.snapshot)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
82
|
+
const tmp = `${this.path}.${process.pid}.tmp`;
|
|
83
|
+
// ⚠️ OWNER-ONLY. These files hold names, emails and phone numbers.
|
|
84
|
+
writeFileSync(tmp, JSON.stringify(this.snapshot()), { mode: 0o600 });
|
|
85
|
+
chmodSync(tmp, 0o600);
|
|
86
|
+
renameSync(tmp, this.path);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// A full disk must not take the conversation down with it: the state is
|
|
90
|
+
// still right in memory, and the next change tries again.
|
|
91
|
+
console.error(`[state] could not write ${this.path}: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Where the stores of one deployment keep their files - or nothing, when it has no directory. */
|
|
96
|
+
export class StateDir {
|
|
97
|
+
dir;
|
|
98
|
+
files = [];
|
|
99
|
+
constructor(dir) {
|
|
100
|
+
this.dir = dir;
|
|
101
|
+
}
|
|
102
|
+
get persisted() {
|
|
103
|
+
return this.dir !== undefined;
|
|
104
|
+
}
|
|
105
|
+
file(name) {
|
|
106
|
+
if (!this.dir)
|
|
107
|
+
return undefined;
|
|
108
|
+
const f = new StateFile(join(this.dir, name));
|
|
109
|
+
this.files.push(f);
|
|
110
|
+
return f;
|
|
111
|
+
}
|
|
112
|
+
/** Everything still waiting, written now - on a clean stop. */
|
|
113
|
+
flush() {
|
|
114
|
+
for (const f of this.files)
|
|
115
|
+
f.flush();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
CHANGED
|
@@ -774,6 +774,8 @@ export type ToolContext = {
|
|
|
774
774
|
* cart was empty. See trip-ids.ts.
|
|
775
775
|
*/
|
|
776
776
|
tripKey?: string;
|
|
777
|
+
/** The client's network address, as the edge proxy saw it - for the booking limits only. */
|
|
778
|
+
ip?: string;
|
|
777
779
|
};
|
|
778
780
|
export type ToolAnswer = {
|
|
779
781
|
structured: Record<string, unknown>;
|
|
@@ -785,5 +787,16 @@ export type ToolAnswer = {
|
|
|
785
787
|
* read better as hotels, grouped by city.
|
|
786
788
|
*/
|
|
787
789
|
export declare const CITY_PICKER_MIN = 4;
|
|
790
|
+
/**
|
|
791
|
+
* The answer when the card names a trip this connector no longer knows.
|
|
792
|
+
*
|
|
793
|
+
* ⚠️ SAID, NOT PAPERED OVER. An unknown id used to fall back to the card's own
|
|
794
|
+
* connection - a DIFFERENT trip from the conversation's - and the card took
|
|
795
|
+
* that trip's id as its own. Rooms added on the card then went into a trip the
|
|
796
|
+
* assistant could not see, and "book it" was answered "the trip is empty"
|
|
797
|
+
* (2026-10-05 audit). The way back is the conversation's own trip, which the
|
|
798
|
+
* assistant's next answer hands the card.
|
|
799
|
+
*/
|
|
800
|
+
export declare const tripGone: (deps: ToolDeps) => ToolAnswer;
|
|
788
801
|
export declare const handleTool: (deps: ToolDeps, name: string, rawArgs: unknown, ctx: ToolContext) => Promise<ToolAnswer>;
|
|
789
802
|
//# sourceMappingURL=tools.d.ts.map
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EAOzB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAgBhD,OAAO,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -5,7 +5,7 @@ import { DESCRIPTION_MAX, POLICY_MAX, hotelText } from './fence.js';
|
|
|
5
5
|
import { formatMoney, money, multiplyMoney, toMinor } from './money.js';
|
|
6
6
|
import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, compareHotels, } from './present.js';
|
|
7
7
|
import { regionOf } from './regions.js';
|
|
8
|
-
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, clampCount, tripKey, } from './trip.js';
|
|
8
|
+
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, TRIP_TTL_MS, clampCount, tripKey, } from './trip.js';
|
|
9
9
|
import { DEMO_NOTE } from './vocab.js';
|
|
10
10
|
// The eleven things a guest can do: find hotels, look at one on dates, see what
|
|
11
11
|
// else it offers, build a trip, ask for the rooms, and deal with a booking they
|
|
@@ -114,6 +114,23 @@ const forCapabilities = (tools, n, v, caps) => {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
if (t.name === n.requestBooking) {
|
|
117
|
+
if (caps.requiresPhone) {
|
|
118
|
+
const schema = out.inputSchema;
|
|
119
|
+
out = {
|
|
120
|
+
...out,
|
|
121
|
+
inputSchema: {
|
|
122
|
+
...schema,
|
|
123
|
+
properties: {
|
|
124
|
+
...(schema.properties ?? {}),
|
|
125
|
+
phone: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: "The guest's phone number, for the hotel to reach them",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: [...new Set([...(schema.required ?? []), 'phone'])],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
117
134
|
if (!caps.members)
|
|
118
135
|
out = { ...out, inputSchema: without(out.inputSchema, 'rememberRequests') };
|
|
119
136
|
if (caps.booking === 'pay-at-hotel') {
|
|
@@ -982,11 +999,21 @@ const common = (deps, key) => ({
|
|
|
982
999
|
? Object.fromEntries(Object.entries(deps.tools).filter(([, name]) => deps.offered?.has(name)))
|
|
983
1000
|
: deps.tools,
|
|
984
1001
|
bookingMode: (deps.backend.capabilities ?? FULL_CAPABILITIES).booking,
|
|
1002
|
+
...((deps.backend.capabilities ?? FULL_CAPABILITIES).requiresPhone
|
|
1003
|
+
? { phoneRequired: true }
|
|
1004
|
+
: {}),
|
|
985
1005
|
...(deps.vocab.sandbox ? { sandbox: true } : {}),
|
|
986
1006
|
vocab: { noun: deps.vocab.noun, nounPlural: deps.vocab.nounPlural },
|
|
987
1007
|
...(deps.vocab.demo ? { demo: true, demoNote: DEMO_NOTE } : { demo: false }),
|
|
988
1008
|
...(key ? memberBadge(deps, key) : {}),
|
|
989
1009
|
});
|
|
1010
|
+
/**
|
|
1011
|
+
* When a room stops being free to cancel, the way a guest reads it: the
|
|
1012
|
+
* hotel's own clock when the backend knows it, else the instant in UTC - never
|
|
1013
|
+
* the date alone (see `atHotelTime`).
|
|
1014
|
+
*/
|
|
1015
|
+
const freeUntilText = (r) => r.freeCancellationLocal ??
|
|
1016
|
+
`${(r.freeCancellationUntil ?? '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
990
1017
|
/** One line of a trip, flattened for the card. Prices are strings: the card never does arithmetic. */
|
|
991
1018
|
const quoteForCard = (quote) => ({
|
|
992
1019
|
lines: quote.lines.map((l) => ({
|
|
@@ -1328,6 +1355,8 @@ const identity = (deps, ctx, key, args = {}) => {
|
|
|
1328
1355
|
// checks, not authentication, and it must never become a way to read
|
|
1329
1356
|
// somebody else's booking.
|
|
1330
1357
|
...(member ? { member } : {}),
|
|
1358
|
+
// Only a booking limit reads it - see `Identity.ip`.
|
|
1359
|
+
...(ctx.ip ? { ip: ctx.ip } : {}),
|
|
1331
1360
|
};
|
|
1332
1361
|
};
|
|
1333
1362
|
/**
|
|
@@ -1682,6 +1711,23 @@ const suggestionsFor = async (deps, d) => {
|
|
|
1682
1711
|
})
|
|
1683
1712
|
.slice(0, 3);
|
|
1684
1713
|
};
|
|
1714
|
+
/**
|
|
1715
|
+
* The answer when the card names a trip this connector no longer knows.
|
|
1716
|
+
*
|
|
1717
|
+
* ⚠️ SAID, NOT PAPERED OVER. An unknown id used to fall back to the card's own
|
|
1718
|
+
* connection - a DIFFERENT trip from the conversation's - and the card took
|
|
1719
|
+
* that trip's id as its own. Rooms added on the card then went into a trip the
|
|
1720
|
+
* assistant could not see, and "book it" was answered "the trip is empty"
|
|
1721
|
+
* (2026-10-05 audit). The way back is the conversation's own trip, which the
|
|
1722
|
+
* assistant's next answer hands the card.
|
|
1723
|
+
*/
|
|
1724
|
+
export const tripGone = (deps) => {
|
|
1725
|
+
const detail = `The trip this card was showing is no longer kept - a trip is kept for ${TRIP_TTL_MS / 3_600_000} hours after its last change. Ask the assistant to show the trip again.`;
|
|
1726
|
+
return {
|
|
1727
|
+
structured: { view: 'error', ...common(deps), error: 'trip-gone', detail },
|
|
1728
|
+
text: detail,
|
|
1729
|
+
};
|
|
1730
|
+
};
|
|
1685
1731
|
export const handleTool = async (deps, name, rawArgs, ctx) => {
|
|
1686
1732
|
const answer = await answerTool(deps, name, rawArgs, ctx);
|
|
1687
1733
|
const suggestions = await suggestionsFor(deps, answer.structured).catch(() => []);
|
|
@@ -2166,6 +2212,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2166
2212
|
// string, not by anyone looking at it.
|
|
2167
2213
|
...(r.publicNightly ? { publicNightly: formatMoney(r.publicNightly) } : {}),
|
|
2168
2214
|
...(r.publicStayTotal ? { publicStayTotal: formatMoney(r.publicStayTotal) } : {}),
|
|
2215
|
+
// ⚠️ AND STRUCK THROUGH FOR AS MANY ROOMS AS THE PRICE BESIDE IT. One
|
|
2216
|
+
// room's public rate drawn over two rooms' member rate put a "was" price
|
|
2217
|
+
// BELOW the price a member pays (2026-10-05 audit).
|
|
2218
|
+
...(r.publicStayTotal && roomsWanted > 1
|
|
2219
|
+
? { publicRoomsTotal: formatMoney(multiplyMoney(r.publicStayTotal, roomsWanted)) }
|
|
2220
|
+
: {}),
|
|
2221
|
+
...(r.freeCancellationUntil ? { freeCancellationLocal: freeUntilText(r) } : {}),
|
|
2169
2222
|
}));
|
|
2170
2223
|
return {
|
|
2171
2224
|
structured: {
|
|
@@ -2178,7 +2231,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2178
2231
|
},
|
|
2179
2232
|
text: [
|
|
2180
2233
|
`**${h.name}** · ${enough.length} room ${enough.length === 1 ? 'type' : 'types'} for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}.`,
|
|
2181
|
-
...enough.map((r) => `- ${r.name} — ${formatMoney(r.nightly)} a night, ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${roomsWanted > 1 ? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)` : ''}${r.freeCancellationUntil ? `, free to cancel until ${r
|
|
2234
|
+
...enough.map((r) => `- ${r.name} — ${formatMoney(r.nightly)} a night, ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${roomsWanted > 1 ? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)` : ''}${r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? ', the free cancellation window has closed - cancelling now has a fee' : ', non-refundable'}`),
|
|
2182
2235
|
flex.text,
|
|
2183
2236
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2184
2237
|
]
|
|
@@ -2259,7 +2312,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2259
2312
|
: rooms.length
|
|
2260
2313
|
? [
|
|
2261
2314
|
`Rooms for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}:`,
|
|
2262
|
-
...rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${r.freeCancellationUntil ? `, free to cancel until ${r
|
|
2315
|
+
...rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? '' : ', non-refundable'}`),
|
|
2263
2316
|
].join('\n')
|
|
2264
2317
|
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
2265
2318
|
extras.length
|
|
@@ -2487,11 +2540,22 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2487
2540
|
};
|
|
2488
2541
|
}
|
|
2489
2542
|
const guest = { name: str(args.name), email: str(args.email) };
|
|
2490
|
-
|
|
2543
|
+
// A phone where the backend needs one: a booking paid at the hotel and
|
|
2544
|
+
// guaranteed by nobody, which the hotel has to be able to follow up.
|
|
2545
|
+
const phoneNeeded = !!(deps.backend.capabilities ?? FULL_CAPABILITIES).requiresPhone;
|
|
2546
|
+
const phone = str(args.phone);
|
|
2547
|
+
const missing = [
|
|
2548
|
+
!guest.name && 'a name',
|
|
2549
|
+
!guest.email.includes('@') && 'an email',
|
|
2550
|
+
phoneNeeded && phone.replace(/\D/g, '').length < 6 && 'a phone number',
|
|
2551
|
+
].filter((x) => typeof x === 'string');
|
|
2491
2552
|
if (missing.length) {
|
|
2553
|
+
const said = missing.length > 1
|
|
2554
|
+
? `${missing.slice(0, -1).join(', ')} and ${missing.at(-1)}`
|
|
2555
|
+
: (missing[0] ?? '');
|
|
2492
2556
|
return {
|
|
2493
2557
|
structured: { view: 'needs-guest', ...common(deps, key), missing },
|
|
2494
|
-
text: `The booking needs ${
|
|
2558
|
+
text: `The booking needs ${said}. Ask the guest for ${missing.length > 1 ? 'them' : 'it'} - do not use a ${phoneNeeded ? 'name, email or phone number' : 'name or email'} you know from anywhere else.`,
|
|
2495
2559
|
};
|
|
2496
2560
|
}
|
|
2497
2561
|
const { requests, dropped } = readRequests(args);
|