@curless/hotel-storefront 0.26.2 → 0.27.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__/audit-p0.test.d.ts +2 -0
- package/dist/__tests__/audit-p0.test.d.ts.map +1 -0
- package/dist/__tests__/audit-p0.test.js +274 -0
- package/dist/__tests__/audit-p0.test.js.map +1 -0
- package/dist/__tests__/card-contract.test.js +15 -0
- package/dist/__tests__/card-contract.test.js.map +1 -1
- package/dist/__tests__/demo-hotels.test.js +2 -1
- package/dist/__tests__/demo-hotels.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +13 -7
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/backend.d.ts +30 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +37 -0
- 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/demo-hotels.d.ts +14 -0
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +100 -12
- package/dist/demo-hotels.js.map +1 -1
- package/dist/limits.d.ts +10 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +25 -2
- package/dist/limits.js.map +1 -1
- package/dist/server.d.ts +19 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +71 -8
- package/dist/server.js.map +1 -1
- package/dist/sessions.d.ts +31 -4
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +66 -15
- package/dist/sessions.js.map +1 -1
- package/dist/state.d.ts +16 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +33 -3
- package/dist/state.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +63 -18
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/state.js
CHANGED
|
@@ -18,10 +18,23 @@ import { dirname, join } from 'node:path';
|
|
|
18
18
|
// rewrite its file on every tool call - so the last half second before a crash
|
|
19
19
|
// can be lost. A clean stop (`flush`, on SIGTERM) loses nothing.
|
|
20
20
|
const WRITE_DELAY_MS = 500;
|
|
21
|
+
/**
|
|
22
|
+
* The most one store may write, in characters of JSON.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ PAST IT, NOTHING IS WRITTEN AND SOMEBODY IS TOLD. Unbounded guest fields
|
|
25
|
+
* once grew the bookings file past what `JSON.stringify` can make at all; the
|
|
26
|
+
* write threw, a line went to a log nobody reads, and every booking from then
|
|
27
|
+
* on lived only in memory until the next deploy forgot it (2026-10-05 audit).
|
|
28
|
+
* Now an over-size snapshot leaves the last good file where it is, the store
|
|
29
|
+
* says it is in trouble, and the demo stops taking bookings it could not keep.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_STATE_CHARS = 32 * 1024 * 1024;
|
|
21
32
|
export class StateFile {
|
|
22
33
|
path;
|
|
23
34
|
timer;
|
|
24
35
|
snapshot;
|
|
36
|
+
/** Why the last write did not happen - cleared by the next one that does. */
|
|
37
|
+
trouble;
|
|
25
38
|
constructor(path) {
|
|
26
39
|
this.path = path;
|
|
27
40
|
}
|
|
@@ -78,18 +91,31 @@ export class StateFile {
|
|
|
78
91
|
if (!this.snapshot)
|
|
79
92
|
return;
|
|
80
93
|
try {
|
|
94
|
+
const text = JSON.stringify(this.snapshot());
|
|
95
|
+
if (text.length > MAX_STATE_CHARS) {
|
|
96
|
+
this.fail(`${text.length} characters, over the ${MAX_STATE_CHARS} a store may write`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
81
99
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
82
100
|
const tmp = `${this.path}.${process.pid}.tmp`;
|
|
83
101
|
// ⚠️ OWNER-ONLY. These files hold names, emails and phone numbers.
|
|
84
|
-
writeFileSync(tmp,
|
|
102
|
+
writeFileSync(tmp, text, { mode: 0o600 });
|
|
85
103
|
chmodSync(tmp, 0o600);
|
|
86
104
|
renameSync(tmp, this.path);
|
|
105
|
+
this.trouble = undefined;
|
|
87
106
|
}
|
|
88
107
|
catch (err) {
|
|
89
108
|
// 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
|
-
|
|
109
|
+
// still right in memory, and the next change tries again - but until one
|
|
110
|
+
// does, the store says so (see `trouble`).
|
|
111
|
+
this.fail(err.message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
fail(why) {
|
|
115
|
+
if (this.trouble !== why) {
|
|
116
|
+
console.error(`[state] NEEDS A PERSON: ${this.path} was not written - ${why}`);
|
|
92
117
|
}
|
|
118
|
+
this.trouble = why;
|
|
93
119
|
}
|
|
94
120
|
}
|
|
95
121
|
/** Where the stores of one deployment keep their files - or nothing, when it has no directory. */
|
|
@@ -114,5 +140,9 @@ export class StateDir {
|
|
|
114
140
|
for (const f of this.files)
|
|
115
141
|
f.flush();
|
|
116
142
|
}
|
|
143
|
+
/** The stores whose last write did not happen, and why - for `/healthz/detail`. */
|
|
144
|
+
troubles() {
|
|
145
|
+
return Object.fromEntries(this.files.filter((f) => f.trouble).map((f) => [f.path, f.trouble ?? '']));
|
|
146
|
+
}
|
|
117
147
|
}
|
|
118
148
|
//# sourceMappingURL=state.js.map
|
package/dist/state.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,8EAA8E;AAC9E,iCAAiC;AACjC,EAAE;AACF,4EAA4E;AAC5E,gFAAgF;AAChF,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,8CAA8C;AAC9C,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,iEAAiE;AAEjE,MAAM,cAAc,GAAG,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
|
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,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_PAGE, FULL_CAPABILITIES, LASTING_REQUESTS, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, } from './backend.js';
|
|
1
|
+
import { DEFAULT_PAGE, FULL_CAPABILITIES, LASTING_REQUESTS, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, } from './backend.js';
|
|
2
2
|
import { isDay, nights as nightsBetween } from './dates.js';
|
|
3
3
|
import { FACILITIES } from './facilities.js';
|
|
4
4
|
import { DESCRIPTION_MAX, POLICY_MAX, hotelText } from './fence.js';
|
|
@@ -1340,14 +1340,17 @@ const dateFlex = (dates, stay) => {
|
|
|
1340
1340
|
const identity = (deps, ctx, key, args = {}) => {
|
|
1341
1341
|
const email = str(args.email);
|
|
1342
1342
|
const member = deps.members.read(key);
|
|
1343
|
+
const own = tripKey(ctx.session, deps.vocab.brand);
|
|
1343
1344
|
return {
|
|
1344
|
-
// ⚠️ THE
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1345
|
+
// ⚠️ THE CALLER'S OWN SESSION, NEVER THE TRIP IT NAMED. This was the trip
|
|
1346
|
+
// key, so that the card - on a connection of its own - could cancel what
|
|
1347
|
+
// the conversation booked. But a trip id travels in every card answer, and
|
|
1348
|
+
// it opened every booking of that conversation, names and emails and all,
|
|
1349
|
+
// from any connection or none (2026-10-05 audit). A trip id is for the
|
|
1350
|
+
// trip; a booking opens by its conversation, or by its code AND email,
|
|
1351
|
+
// which is what the card now sends.
|
|
1352
|
+
sessionId: own,
|
|
1353
|
+
...(key !== own ? { tripOwner: key } : {}),
|
|
1351
1354
|
...(email ? { email } : {}),
|
|
1352
1355
|
// ⚠️ THE MEMBER BUYS A RATE, NOT A LOOKUP. `listBookings` and the
|
|
1353
1356
|
// cancellation tools still key on the conversation and on the email the
|
|
@@ -1391,6 +1394,29 @@ const remember = (deps, k, amount) => {
|
|
|
1391
1394
|
m.delete(oldest);
|
|
1392
1395
|
}
|
|
1393
1396
|
};
|
|
1397
|
+
/**
|
|
1398
|
+
* What a backend's refusal says, for the guest - or, when the failure is the
|
|
1399
|
+
* code's own, a sentence that says nothing about the code.
|
|
1400
|
+
*
|
|
1401
|
+
* ⚠️ A REFUSAL IS WORDS FOR THE GUEST; A FAULT IS NOT. Every backend refuses
|
|
1402
|
+
* with a plain Error, or one carrying a `code`, and those words are written to
|
|
1403
|
+
* be read back ("Nothing was booked: ..."). A TypeError's words - "Cannot read
|
|
1404
|
+
* properties of null" - were read back too (2026-10-05 audit); they go to the
|
|
1405
|
+
* log now.
|
|
1406
|
+
*/
|
|
1407
|
+
const guestWords = (err, fault = 'That could not be done just now - nothing was changed. Try again in a moment.') => {
|
|
1408
|
+
const e = err;
|
|
1409
|
+
if (e instanceof Error && (typeof e.code === 'string' || e.constructor === Error))
|
|
1410
|
+
return e.message;
|
|
1411
|
+
console.error('[tools] failed:', err);
|
|
1412
|
+
return fault;
|
|
1413
|
+
};
|
|
1414
|
+
/**
|
|
1415
|
+
* A booking's own fault sentence. ⚠️ IT CANNOT SAY "NOTHING WAS CHANGED": the
|
|
1416
|
+
* fault may come after the backend has booked (华住's review, 2026-10-05), and
|
|
1417
|
+
* a guest told nothing happened books again.
|
|
1418
|
+
*/
|
|
1419
|
+
const BOOKING_FAULT = 'It is not certain whether that booked - list the bookings before asking for it again.';
|
|
1394
1420
|
/** Said when a lookup has missed too often - see limits.ts. */
|
|
1395
1421
|
const TOO_MANY_TRIES = 'Too many codes and emails that did not match, so looking a booking up by email is paused for a quarter of an hour. Check the code and the email on the confirmation, then try again.';
|
|
1396
1422
|
/**
|
|
@@ -1401,8 +1427,19 @@ const TOO_MANY_TRIES = 'Too many codes and emails that did not match, so looking
|
|
|
1401
1427
|
* email; after a few, lookups by email wait (limits.ts). A lookup of this
|
|
1402
1428
|
* conversation's own booking, with no email, is never counted and never waits.
|
|
1403
1429
|
*/
|
|
1404
|
-
const guarded = async (deps,
|
|
1405
|
-
|
|
1430
|
+
const guarded = async (deps, ctx, who, code, run) => {
|
|
1431
|
+
// ⚠️ NOT ONLY THE CONVERSATION. Counted against the conversation alone, a
|
|
1432
|
+
// guesser started a new one - or sent none - after every five (2026-10-05
|
|
1433
|
+
// audit). So also against the CODE, which stops "I know the code, let me
|
|
1434
|
+
// guess the email", and the network address, which stops the rest.
|
|
1435
|
+
const keys = who.email
|
|
1436
|
+
? [
|
|
1437
|
+
`lookup|${who.sessionId}`,
|
|
1438
|
+
`email|${who.email.trim().toLowerCase()}`,
|
|
1439
|
+
...(code ? [`code|${code.trim().toLowerCase()}`] : []),
|
|
1440
|
+
...(ctx.ip ? [`ip|${ctx.ip}`] : []),
|
|
1441
|
+
]
|
|
1442
|
+
: [];
|
|
1406
1443
|
if (keys.length && deps.misses?.blocked(keys))
|
|
1407
1444
|
return { found: null, tooMany: true };
|
|
1408
1445
|
const found = await run();
|
|
@@ -2353,7 +2390,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2353
2390
|
const code = str(args.booking);
|
|
2354
2391
|
const who = identity(deps, ctx, key, args);
|
|
2355
2392
|
const { found, tooMany } = code
|
|
2356
|
-
? await guarded(deps,
|
|
2393
|
+
? await guarded(deps, ctx, who, code, () => deps.backend.findBooking(code, who))
|
|
2357
2394
|
: { found: null, tooMany: false };
|
|
2358
2395
|
const booked = found && (found.status === 'requested' || found.status === 'confirmed') ? found : undefined;
|
|
2359
2396
|
if (code && found && !booked) {
|
|
@@ -2548,6 +2585,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2548
2585
|
!guest.name && 'a name',
|
|
2549
2586
|
!guest.email.includes('@') && 'an email',
|
|
2550
2587
|
phoneNeeded && phone.replace(/\D/g, '').length < 6 && 'a phone number',
|
|
2588
|
+
// Too long is as unusable as missing - see GUEST_LIMITS.
|
|
2589
|
+
...guestTooLong({ ...guest, phone }),
|
|
2551
2590
|
].filter((x) => typeof x === 'string');
|
|
2552
2591
|
if (missing.length) {
|
|
2553
2592
|
const said = missing.length > 1
|
|
@@ -2674,14 +2713,15 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2674
2713
|
};
|
|
2675
2714
|
}
|
|
2676
2715
|
catch (err) {
|
|
2716
|
+
const words = guestWords(err, BOOKING_FAULT);
|
|
2677
2717
|
return {
|
|
2678
2718
|
structured: {
|
|
2679
2719
|
view: 'error',
|
|
2680
2720
|
...common(deps, key),
|
|
2681
2721
|
error: 'not-bookable',
|
|
2682
|
-
detail:
|
|
2722
|
+
detail: words,
|
|
2683
2723
|
},
|
|
2684
|
-
text:
|
|
2724
|
+
text: words,
|
|
2685
2725
|
};
|
|
2686
2726
|
}
|
|
2687
2727
|
}
|
|
@@ -2691,7 +2731,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2691
2731
|
// Elsewhere, only with the code AND the email - see `listBookings`.
|
|
2692
2732
|
const code = str(args.booking);
|
|
2693
2733
|
const { found: there, tooMany } = code && str(args.email)
|
|
2694
|
-
? await guarded(deps,
|
|
2734
|
+
? await guarded(deps, ctx, who, code, () => deps.backend.findBooking(code, who))
|
|
2695
2735
|
: { found: null, tooMany: false };
|
|
2696
2736
|
const bookings = there && !here.some((b) => b.id === there.id) ? [there, ...here] : here;
|
|
2697
2737
|
// ⚠️ A MISS IS SAID EVEN WHEN THERE IS SOMETHING TO LIST. With two
|
|
@@ -2737,7 +2777,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2737
2777
|
...(str(args.extra) ? { extra: str(args.extra) } : {}),
|
|
2738
2778
|
...(typeof args.quantity === 'number' ? { quantity: args.quantity } : {}),
|
|
2739
2779
|
};
|
|
2740
|
-
const { found: preview, tooMany } = await guarded(deps,
|
|
2780
|
+
const { found: preview, tooMany } = await guarded(deps, ctx, who, ref, () => deps.backend.previewChange(ref, change, who));
|
|
2741
2781
|
if (!preview) {
|
|
2742
2782
|
return {
|
|
2743
2783
|
structured: {
|
|
@@ -2859,13 +2899,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2859
2899
|
};
|
|
2860
2900
|
}
|
|
2861
2901
|
catch (err) {
|
|
2862
|
-
return { ...asPreview, text: err
|
|
2902
|
+
return { ...asPreview, text: guestWords(err) };
|
|
2863
2903
|
}
|
|
2864
2904
|
}
|
|
2865
2905
|
if (name === t.previewCancellation || name === t.cancelBooking) {
|
|
2866
2906
|
const ref = str(args.booking);
|
|
2867
2907
|
const who = identity(deps, ctx, key, args);
|
|
2868
|
-
const { found: preview, tooMany } = await guarded(deps,
|
|
2908
|
+
const { found: preview, tooMany } = await guarded(deps, ctx, who, ref, () => deps.backend.previewCancellation(ref, who));
|
|
2869
2909
|
if (!preview) {
|
|
2870
2910
|
return {
|
|
2871
2911
|
structured: {
|
|
@@ -3051,7 +3091,12 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3051
3091
|
const surname = str(args.surname);
|
|
3052
3092
|
// Counted like a booking lookup - see limits.ts. A number and a surname
|
|
3053
3093
|
// read a tier, a point balance and saved preferences.
|
|
3054
|
-
const tries = [
|
|
3094
|
+
const tries = [
|
|
3095
|
+
`signin|${key}`,
|
|
3096
|
+
`member|${number.replace(/\s+/g, '').toUpperCase()}`,
|
|
3097
|
+
// The network too, as for bookings - see `guarded`.
|
|
3098
|
+
...(ctx.ip ? [`ip|${ctx.ip}`] : []),
|
|
3099
|
+
];
|
|
3055
3100
|
if (deps.misses?.blocked(tries)) {
|
|
3056
3101
|
return {
|
|
3057
3102
|
structured: {
|