@rubytech/create-maxy-code 0.1.352 → 0.1.354
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/package.json +1 -1
- package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +96 -14
- package/payload/platform/lib/admin-access-password/dist/index.d.ts +20 -4
- package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admin-access-password/dist/index.js +43 -4
- package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
- package/payload/platform/lib/admin-access-password/src/index.ts +67 -4
- package/payload/platform/lib/admins-write/__tests__/index.test.ts +182 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts +65 -11
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +93 -31
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +133 -36
- package/payload/platform/package-lock.json +76 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +15 -3
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +2 -2
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +41 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +236 -47
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts +27 -9
- package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/call.js +102 -29
- package/payload/platform/plugins/connector/mcp/dist/lib/call.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts +17 -6
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js +28 -10
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/package.json +13 -3
- package/payload/platform/plugins/docs/references/troubleshooting.md +15 -0
- package/payload/server/{chunk-QAKVFSEJ.js → chunk-7GNXBVGY.js} +38 -11
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-lt72mYrz.js → AdminLoginScreens-6WgO-Z3m.js} +1 -1
- package/payload/server/public/assets/{AdminShell-B-GeoG3j.js → AdminShell-GGnPXCOe.js} +1 -1
- package/payload/server/public/assets/{Checkbox-DoGMXVpF.js → Checkbox-D3XFVUMT.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-CFWXdnjB.css +1 -0
- package/payload/server/public/assets/{OperatorConversations-CfG1EYyP.js → OperatorConversations-DeZ2wDEK.js} +1 -1
- package/payload/server/public/assets/{admin-DzLxv-nz.js → admin-B2yNhVvH.js} +1 -1
- package/payload/server/public/assets/{browser-Cyi6v8BS.js → browser-ByaQ0ItQ.js} +1 -1
- package/payload/server/public/assets/calendar-BjqCT8cW.js +1 -0
- package/payload/server/public/assets/chat-kl-Ips-o.js +1 -0
- package/payload/server/public/assets/{data-CrJZjlQ1.js → data-p9p-181v.js} +1 -1
- package/payload/server/public/assets/{graph-CutaO0w1.js → graph-BuunC8eP.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CH0sWvcA.js → graph-labels-BF3Ak8q0.js} +1 -1
- package/payload/server/public/assets/{operator-MASl7IqX.js → operator-D7c8mN4J.js} +1 -1
- package/payload/server/public/assets/page-C6uMZdT1.js +32 -0
- package/payload/server/public/assets/{public-77Ubicd7.js → public-B9_UhjEa.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +1127 -1042
- package/payload/server/public/assets/OperatorConversations-C8G6Gfgu.css +0 -1
- package/payload/server/public/assets/calendar-CA3_Dm8j.js +0 -1
- package/payload/server/public/assets/chat-B9aNK1FG.js +0 -1
- package/payload/server/public/assets/page-CZy33qUH.js +0 -30
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:7cdf605d8c40fcc1e028ad43fe5411edcd9c422101aa9a3d6564a83798ad243a
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -4118,6 +4118,21 @@ grep -E "adminuser-self-heal|graph-write-gate.*reject" <server.log>
|
|
|
4118
4118
|
|
|
4119
4119
|
The `subReason=admin-user-no-accountid` path should be impossible on any install whose admin server has booted at least once after the boot self-heal shipped — if it fires, the diagnostic recipe is the cross-check above, not "rerun the heal."
|
|
4120
4120
|
|
|
4121
|
+
## An admin can no longer log in, or a credential changed and you don't know who/when
|
|
4122
|
+
|
|
4123
|
+
**Symptom:** An admin's remote password or PIN stopped working, or an admin row vanished from `users.json` while `account.json` still lists them — with no obvious cause in `server.log`.
|
|
4124
|
+
|
|
4125
|
+
**Diagnose:** every credential-store write is recorded in a dedicated append-only log, separate from `server.log`:
|
|
4126
|
+
|
|
4127
|
+
```
|
|
4128
|
+
grep -E '\[users-audit\]|\[users-reconcile\]' ~/.<brand>/logs/users-audit.log ~/.<brand>/logs/server.log
|
|
4129
|
+
```
|
|
4130
|
+
|
|
4131
|
+
- `[users-audit] action=<…> actor=<id8> [session=<id8>] field=<…> rowsBefore=<…> rowsAfter=<…> ts=<…>` — one line per write. Filter by `actor=` or `session=` to attribute a single mutation; a credential change with an agent behind it carries `session=` even when that session's transcript is gone. A `users.json` mtime change with **no** `[users-audit]` line in the same window means an out-of-band write (something edited the file directly, outside every audited path).
|
|
4132
|
+
- `[users-reconcile] op=ok admins=<n>` — emitted at boot and hourly; the stores agree. Absence of any `op=ok`/`op=divergence` line across an interval is itself the failure signal (the tick stopped).
|
|
4133
|
+
- `[users-reconcile] op=divergence accountOnly=<id8,…> usersOnly=<id8,…>` — the stores disagree. `accountOnly` is the incident shape: an admin is in `account.json` with no `users.json` row (they will fail every login). Cross-check the most recent `[users-audit] rowsAfter` against the divergence to see whether an audited delete caused it or an out-of-band write did. `usersOnly` is a `users.json` row no account references.
|
|
4134
|
+
- `[users-reconcile] op=error detail=…` — the reconcile itself could not read a store; the divergence state is unknown until the next clean tick.
|
|
4135
|
+
|
|
4121
4136
|
## Fresh install opens to "Set your remote password" on the LAN URL
|
|
4122
4137
|
|
|
4123
4138
|
**Symptom:** On a brand-new device, the LAN URL printed by `create-maxy` (e.g. `http://maxy.local:19200`) opens to a remote-password setup page instead of admin onboarding. This was an earlier regression and should not occur on any install built.
|
|
@@ -34,7 +34,7 @@ The public page shows open slots computed as the weekly window minus busy `:Meet
|
|
|
34
34
|
<accountDir>/calendar-availability.json
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Collect from the operator, then write the file: the account `timezone` (IANA), the meeting `durationMins`, the `bufferMins` between meetings, and the `weekly` window as `{ mon: [["09:00","17:00"]], ... }` with an empty array for days that are closed. Add the `apiBase` (the brand tunnel origin that serves free/busy, `https://<brand-tunnel-domain>`), the `bookingDomain` (the custom domain the page will answer on), and `bookingDbName` (the D1 database name chosen below). Re-running this skill with new numbers is how availability is changed later; there is no separate editor.
|
|
37
|
+
Collect from the operator, then write the file: the account `timezone` (IANA), the meeting `durationMins`, the `bufferMins` between meetings, and the `weekly` window as `{ mon: [["09:00","17:00"]], ... }` with an empty array for days that are closed. Also collect `daysAhead` (the booking horizon — a positive integer count of days the page lets visitors book into, e.g. `14`) and `allowMultiSlot` (a boolean: may a visitor combine consecutive open slots into one longer back-to-back booking? default `false`). Add the `apiBase` (the brand tunnel origin that serves free/busy, `https://<brand-tunnel-domain>`), the `bookingDomain` (the custom domain the page will answer on), and `bookingDbName` (the D1 database name chosen below). Re-running this skill with new numbers is how availability — including the horizon and the multi-slot choice — is changed later; there is no separate editor.
|
|
38
38
|
|
|
39
39
|
## Assemble the canonical tree
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ Copy the skill template into the one canonical Pages source path `site-deploy` e
|
|
|
44
44
|
<accountDir>/pages/<project>/
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `wrangler.toml`, and `schema.sql`.
|
|
47
|
+
The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `wrangler.toml`, and `schema.sql`. The substitutions that make it concrete are: in `public/index.html`, set the `<meta name="api-base">` content to the `apiBase` value (the booking page fetches free/busy cross-origin from the brand tunnel), set `<meta name="days-ahead">` content to `daysAhead`, and set `<meta name="allow-multi-slot">` content to `allowMultiSlot` as the literal `true` or `false`; and fill `wrangler.toml`'s `__PROJECT_NAME__`, `__D1_DATABASE_NAME__`, and `__D1_DATABASE_ID__` from the project name and the database created below. Leave no `__...__` placeholder in the assembled tree.
|
|
48
48
|
|
|
49
49
|
## Provision the D1 bookings table
|
|
50
50
|
|
package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css
CHANGED
|
@@ -41,6 +41,44 @@ body {
|
|
|
41
41
|
.bk-day { margin-bottom: 20px; }
|
|
42
42
|
.bk-day-title { font-size: 0.95rem; margin: 0 0 8px; color: var(--bk-ink); }
|
|
43
43
|
.bk-day-slots { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
44
|
+
.bk-day-empty { margin: 0; font-size: 0.85rem; color: var(--bk-muted); }
|
|
45
|
+
|
|
46
|
+
/* Five-day window pager. */
|
|
47
|
+
.bk-nav {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
gap: 12px;
|
|
52
|
+
margin-bottom: 16px;
|
|
53
|
+
}
|
|
54
|
+
.bk-nav-label { font-size: 0.9rem; color: var(--bk-muted); }
|
|
55
|
+
.bk-arrow {
|
|
56
|
+
border: 1px solid var(--bk-line);
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: var(--bk-ink);
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
width: 36px;
|
|
61
|
+
height: 36px;
|
|
62
|
+
font-size: 1.1rem;
|
|
63
|
+
line-height: 1;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
.bk-arrow:hover:not(:disabled) { border-color: var(--bk-accent); color: var(--bk-accent); }
|
|
67
|
+
.bk-arrow:disabled { opacity: 0.35; cursor: default; }
|
|
68
|
+
|
|
69
|
+
/* Extend / shorten a back-to-back multi-slot booking (operator-enabled). */
|
|
70
|
+
.bk-extend { display: flex; gap: 8px; margin: 0 0 4px; }
|
|
71
|
+
.bk-extend-btn {
|
|
72
|
+
border: 1px solid var(--bk-accent);
|
|
73
|
+
background: transparent;
|
|
74
|
+
color: var(--bk-accent);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 6px 12px;
|
|
77
|
+
font-size: 0.85rem;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
.bk-extend-btn:hover:not(:disabled) { background: var(--bk-accent); color: var(--bk-accent-ink); }
|
|
81
|
+
.bk-extend-btn:disabled { opacity: 0.4; cursor: default; }
|
|
44
82
|
|
|
45
83
|
.bk-slot {
|
|
46
84
|
border: 1px solid var(--bk-accent);
|
|
@@ -54,6 +92,9 @@ body {
|
|
|
54
92
|
.bk-slot:hover { background: var(--bk-accent); color: var(--bk-accent-ink); }
|
|
55
93
|
|
|
56
94
|
.bk-form { display: flex; flex-direction: column; gap: 14px; }
|
|
95
|
+
/* The form's display:flex would otherwise beat the UA [hidden] rule and show
|
|
96
|
+
the form before a slot is picked; this keeps `hidden` authoritative. */
|
|
97
|
+
.bk-form[hidden] { display: none; }
|
|
57
98
|
.bk-chosen { font-weight: 600; margin: 0 0 4px; }
|
|
58
99
|
.bk-label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--bk-muted); }
|
|
59
100
|
.bk-input {
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
// Public booking page (calendar-site skill). Dependency-free.
|
|
2
2
|
// 1. Fetch open slots from the brand tunnel free/busy endpoint.
|
|
3
|
-
// 2.
|
|
3
|
+
// 2. Page the horizon five days at a time, let the visitor pick a slot
|
|
4
|
+
// (and, when the operator allows it, extend to back-to-back slots).
|
|
4
5
|
// 3. POST the booking to the same-origin Pages Function (/api/book → D1).
|
|
5
6
|
(function () {
|
|
6
7
|
'use strict'
|
|
7
8
|
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
var WINDOW_DAYS = 5
|
|
10
|
+
|
|
11
|
+
function metaContent(name) {
|
|
12
|
+
var el = document.querySelector('meta[name="' + name + '"]')
|
|
13
|
+
return el ? el.getAttribute('content') : null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Operator-set booking horizon. The skill rewrites the meta at assemble time;
|
|
17
|
+
// fall back to 14 only when it is absent or left unsubstituted, so pages
|
|
18
|
+
// assembled before this change behave exactly as they did.
|
|
19
|
+
var parsedDaysAhead = parseInt(metaContent('days-ahead'), 10)
|
|
20
|
+
var daysAhead = isFinite(parsedDaysAhead) && parsedDaysAhead > 0 ? parsedDaysAhead : 14
|
|
21
|
+
|
|
22
|
+
// Operator choice: may a visitor combine consecutive open slots into one
|
|
23
|
+
// longer booking? Anything other than the literal "true" is off.
|
|
24
|
+
var allowMultiSlot = metaContent('allow-multi-slot') === 'true'
|
|
25
|
+
|
|
26
|
+
var apiBase = metaContent('api-base') || ''
|
|
11
27
|
|
|
12
28
|
var statusEl = document.getElementById('bk-status')
|
|
13
29
|
var slotsEl = document.getElementById('bk-slots')
|
|
@@ -18,7 +34,12 @@
|
|
|
18
34
|
var durationEl = document.getElementById('bk-duration')
|
|
19
35
|
var backBtn = document.getElementById('bk-back')
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
// The slots that share the chosen block's day, the index of the first slot
|
|
38
|
+
// in that block, and how many back-to-back slots it spans. count stays 1
|
|
39
|
+
// unless the operator enabled multi-slot bookings.
|
|
40
|
+
var block = null
|
|
41
|
+
// Extend/shorten controls, built lazily once and reused.
|
|
42
|
+
var multiControls = null
|
|
22
43
|
|
|
23
44
|
function setStatus(msg) {
|
|
24
45
|
statusEl.textContent = msg || ''
|
|
@@ -27,9 +48,57 @@
|
|
|
27
48
|
function fmtDayHeading(d) {
|
|
28
49
|
return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })
|
|
29
50
|
}
|
|
51
|
+
function fmtShortDay(d) {
|
|
52
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
53
|
+
}
|
|
30
54
|
function fmtTime(d) {
|
|
31
55
|
return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
32
56
|
}
|
|
57
|
+
function dayKey(d) {
|
|
58
|
+
return d.toDateString()
|
|
59
|
+
}
|
|
60
|
+
function startOfLocalDay(d) {
|
|
61
|
+
var x = new Date(d)
|
|
62
|
+
x.setHours(0, 0, 0, 0)
|
|
63
|
+
return x
|
|
64
|
+
}
|
|
65
|
+
function clear(el) {
|
|
66
|
+
while (el.firstChild) el.removeChild(el.firstChild)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- horizon state, set once per free/busy load -------------------------
|
|
70
|
+
var allDays = [] // local-midnight Date per calendar day in the horizon
|
|
71
|
+
var slotsByDay = {} // dayKey -> ordered slot array
|
|
72
|
+
var windowStart = 0 // index into allDays, advances in WINDOW_DAYS steps
|
|
73
|
+
|
|
74
|
+
// The first and last slot of the chosen block — the single source of the
|
|
75
|
+
// block's index arithmetic, shared by the formatters and the submit payload.
|
|
76
|
+
function firstSlot() {
|
|
77
|
+
return block.daySlots[block.startIdx]
|
|
78
|
+
}
|
|
79
|
+
function lastSlot() {
|
|
80
|
+
return block.daySlots[block.startIdx + block.count - 1]
|
|
81
|
+
}
|
|
82
|
+
function blockStart() {
|
|
83
|
+
return new Date(firstSlot().start)
|
|
84
|
+
}
|
|
85
|
+
function blockEnd() {
|
|
86
|
+
return new Date(lastSlot().end)
|
|
87
|
+
}
|
|
88
|
+
// The slot immediately after the current block, if it is open and back-to-back.
|
|
89
|
+
function nextContiguousSlot() {
|
|
90
|
+
var next = block.daySlots[block.startIdx + block.count]
|
|
91
|
+
if (!next) return null
|
|
92
|
+
return next.start === lastSlot().end ? next : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fmtChosen() {
|
|
96
|
+
var start = blockStart()
|
|
97
|
+
if (block.count === 1) {
|
|
98
|
+
return fmtDayHeading(start) + ' at ' + fmtTime(start)
|
|
99
|
+
}
|
|
100
|
+
return fmtDayHeading(start) + ', ' + fmtTime(start) + ' – ' + fmtTime(blockEnd())
|
|
101
|
+
}
|
|
33
102
|
|
|
34
103
|
function renderSlots(data) {
|
|
35
104
|
var slots = (data && data.slots) || []
|
|
@@ -37,69 +106,184 @@
|
|
|
37
106
|
durationEl.textContent = data.durationMins + ' minute meeting'
|
|
38
107
|
}
|
|
39
108
|
if (slots.length === 0) {
|
|
40
|
-
setStatus('No open times in the next
|
|
109
|
+
setStatus('No open times in the next ' + daysAhead + ' day' + (daysAhead === 1 ? '' : 's') + '. Please check back later.')
|
|
41
110
|
return
|
|
42
111
|
}
|
|
43
112
|
setStatus('')
|
|
44
113
|
|
|
45
|
-
|
|
46
|
-
|
|
114
|
+
// The full calendar-day list for the horizon, today first. setDate keeps
|
|
115
|
+
// local midnight across DST, unlike adding fixed millisecond spans.
|
|
116
|
+
var today = startOfLocalDay(new Date())
|
|
117
|
+
allDays = []
|
|
118
|
+
for (var i = 0; i < daysAhead; i++) {
|
|
119
|
+
var d = new Date(today)
|
|
120
|
+
d.setDate(today.getDate() + i)
|
|
121
|
+
allDays.push(d)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
slotsByDay = {}
|
|
47
125
|
slots.forEach(function (s) {
|
|
48
|
-
var
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
126
|
+
var k = dayKey(new Date(s.start))
|
|
127
|
+
if (!slotsByDay[k]) slotsByDay[k] = []
|
|
128
|
+
slotsByDay[k].push(s)
|
|
129
|
+
})
|
|
130
|
+
Object.keys(slotsByDay).forEach(function (k) {
|
|
131
|
+
slotsByDay[k].sort(function (a, b) {
|
|
132
|
+
return new Date(a.start) - new Date(b.start)
|
|
133
|
+
})
|
|
55
134
|
})
|
|
56
135
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
136
|
+
windowStart = 0
|
|
137
|
+
console.log('[booking] daysAhead=' + daysAhead + ' allowMultiSlot=' + allowMultiSlot + ' windowStart=' + windowStart)
|
|
138
|
+
renderWindow()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderWindow() {
|
|
142
|
+
clear(slotsEl)
|
|
143
|
+
|
|
144
|
+
var nav = document.createElement('div')
|
|
145
|
+
nav.className = 'bk-nav'
|
|
146
|
+
|
|
147
|
+
var back = document.createElement('button')
|
|
148
|
+
back.type = 'button'
|
|
149
|
+
back.className = 'bk-arrow'
|
|
150
|
+
back.setAttribute('aria-label', 'Previous days')
|
|
151
|
+
back.textContent = '‹'
|
|
152
|
+
back.disabled = windowStart === 0
|
|
153
|
+
back.addEventListener('click', function () {
|
|
154
|
+
windowStart = Math.max(0, windowStart - WINDOW_DAYS)
|
|
155
|
+
console.log('[booking] windowStart=' + windowStart)
|
|
156
|
+
renderWindow()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
var label = document.createElement('span')
|
|
160
|
+
label.className = 'bk-nav-label'
|
|
161
|
+
var lastIdx = Math.min(windowStart + WINDOW_DAYS, allDays.length) - 1
|
|
162
|
+
label.textContent = fmtShortDay(allDays[windowStart]) + ' – ' + fmtShortDay(allDays[lastIdx])
|
|
163
|
+
|
|
164
|
+
var fwd = document.createElement('button')
|
|
165
|
+
fwd.type = 'button'
|
|
166
|
+
fwd.className = 'bk-arrow'
|
|
167
|
+
fwd.setAttribute('aria-label', 'Next days')
|
|
168
|
+
fwd.textContent = '›'
|
|
169
|
+
fwd.disabled = windowStart + WINDOW_DAYS >= allDays.length
|
|
170
|
+
fwd.addEventListener('click', function () {
|
|
171
|
+
// Reachable only when not disabled, so a full step always has room.
|
|
172
|
+
windowStart += WINDOW_DAYS
|
|
173
|
+
console.log('[booking] windowStart=' + windowStart)
|
|
174
|
+
renderWindow()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
nav.appendChild(back)
|
|
178
|
+
nav.appendChild(label)
|
|
179
|
+
nav.appendChild(fwd)
|
|
180
|
+
slotsEl.appendChild(nav)
|
|
181
|
+
|
|
182
|
+
for (var i = windowStart; i < windowStart + WINDOW_DAYS && i < allDays.length; i++) {
|
|
183
|
+
slotsEl.appendChild(renderDayGroup(allDays[i]))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// One day's heading plus its slot buttons (or the empty note). Kept as its own
|
|
188
|
+
// function so each slot button closes over that day's own slot array — a shared
|
|
189
|
+
// loop-scoped var would leave every handler pointing at the last day rendered.
|
|
190
|
+
function renderDayGroup(day) {
|
|
191
|
+
var group = document.createElement('div')
|
|
192
|
+
group.className = 'bk-day'
|
|
193
|
+
|
|
194
|
+
var h = document.createElement('h2')
|
|
195
|
+
h.className = 'bk-day-title'
|
|
196
|
+
h.textContent = fmtDayHeading(day)
|
|
197
|
+
group.appendChild(h)
|
|
198
|
+
|
|
199
|
+
var daySlots = slotsByDay[dayKey(day)]
|
|
200
|
+
if (!daySlots || daySlots.length === 0) {
|
|
201
|
+
var note = document.createElement('p')
|
|
202
|
+
note.className = 'bk-day-empty'
|
|
203
|
+
note.textContent = 'No availability'
|
|
204
|
+
group.appendChild(note)
|
|
205
|
+
return group
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var row = document.createElement('div')
|
|
209
|
+
row.className = 'bk-day-slots'
|
|
210
|
+
daySlots.forEach(function (s, idx) {
|
|
211
|
+
var btn = document.createElement('button')
|
|
212
|
+
btn.type = 'button'
|
|
213
|
+
btn.className = 'bk-slot'
|
|
214
|
+
btn.textContent = fmtTime(new Date(s.start))
|
|
215
|
+
btn.addEventListener('click', function () {
|
|
216
|
+
chooseSlot(daySlots, idx)
|
|
76
217
|
})
|
|
77
|
-
|
|
78
|
-
|
|
218
|
+
row.appendChild(btn)
|
|
219
|
+
})
|
|
220
|
+
group.appendChild(row)
|
|
221
|
+
return group
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ensureMultiControls() {
|
|
225
|
+
if (multiControls) return multiControls
|
|
226
|
+
multiControls = document.createElement('div')
|
|
227
|
+
multiControls.className = 'bk-extend'
|
|
228
|
+
|
|
229
|
+
var shorten = document.createElement('button')
|
|
230
|
+
shorten.type = 'button'
|
|
231
|
+
shorten.className = 'bk-extend-btn'
|
|
232
|
+
shorten.id = 'bk-shorten'
|
|
233
|
+
shorten.textContent = '− Shorten'
|
|
234
|
+
shorten.addEventListener('click', function () {
|
|
235
|
+
if (block.count > 1) {
|
|
236
|
+
block.count--
|
|
237
|
+
refreshChosen()
|
|
238
|
+
}
|
|
79
239
|
})
|
|
240
|
+
|
|
241
|
+
var extend = document.createElement('button')
|
|
242
|
+
extend.type = 'button'
|
|
243
|
+
extend.className = 'bk-extend-btn'
|
|
244
|
+
extend.id = 'bk-extend'
|
|
245
|
+
extend.textContent = '+ Add next slot'
|
|
246
|
+
extend.addEventListener('click', function () {
|
|
247
|
+
if (nextContiguousSlot()) {
|
|
248
|
+
block.count++
|
|
249
|
+
refreshChosen()
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
multiControls.appendChild(shorten)
|
|
254
|
+
multiControls.appendChild(extend)
|
|
255
|
+
chosenEl.insertAdjacentElement('afterend', multiControls)
|
|
256
|
+
return multiControls
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function refreshChosen() {
|
|
260
|
+
chosenEl.textContent = fmtChosen()
|
|
261
|
+
if (!allowMultiSlot) return
|
|
262
|
+
ensureMultiControls()
|
|
263
|
+
document.getElementById('bk-shorten').disabled = block.count <= 1
|
|
264
|
+
document.getElementById('bk-extend').disabled = !nextContiguousSlot()
|
|
80
265
|
}
|
|
81
266
|
|
|
82
|
-
function chooseSlot(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
chosenEl.textContent = fmtDayHeading(start) + ' at ' + fmtTime(start)
|
|
267
|
+
function chooseSlot(daySlots, idx) {
|
|
268
|
+
block = { daySlots: daySlots, startIdx: idx, count: 1 }
|
|
269
|
+
refreshChosen()
|
|
86
270
|
slotsEl.hidden = true
|
|
87
271
|
formEl.hidden = false
|
|
88
272
|
}
|
|
89
273
|
|
|
90
274
|
backBtn.addEventListener('click', function () {
|
|
91
|
-
|
|
275
|
+
block = null
|
|
92
276
|
formEl.hidden = true
|
|
93
277
|
slotsEl.hidden = false
|
|
94
278
|
})
|
|
95
279
|
|
|
96
280
|
formEl.addEventListener('submit', function (e) {
|
|
97
281
|
e.preventDefault()
|
|
98
|
-
if (!
|
|
282
|
+
if (!block) return
|
|
99
283
|
var fd = new FormData(formEl)
|
|
100
284
|
var payload = {
|
|
101
|
-
slotStart:
|
|
102
|
-
slotEnd:
|
|
285
|
+
slotStart: firstSlot().start,
|
|
286
|
+
slotEnd: lastSlot().end,
|
|
103
287
|
name: fd.get('name'),
|
|
104
288
|
email: fd.get('email'),
|
|
105
289
|
note: fd.get('note'),
|
|
@@ -125,8 +309,7 @@
|
|
|
125
309
|
}
|
|
126
310
|
formEl.hidden = true
|
|
127
311
|
doneEl.hidden = false
|
|
128
|
-
|
|
129
|
-
doneMsg.textContent = 'Confirmed for ' + fmtDayHeading(start) + ' at ' + fmtTime(start) + '. A confirmation will follow by email.'
|
|
312
|
+
doneMsg.textContent = 'Confirmed for ' + fmtChosen() + '. A confirmation will follow by email.'
|
|
130
313
|
})
|
|
131
314
|
.catch(function (err) {
|
|
132
315
|
submitBtn.disabled = false
|
|
@@ -137,8 +320,14 @@
|
|
|
137
320
|
|
|
138
321
|
function load() {
|
|
139
322
|
setStatus('Loading available times…')
|
|
323
|
+
// Fetch exactly the horizon the page renders: from now through the end of
|
|
324
|
+
// the last calendar day in [today, today + daysAhead). Bounding `to` to a
|
|
325
|
+
// local-midnight day edge (not now + daysAhead*24h) keeps the fetched range
|
|
326
|
+
// and the rendered `allDays` window in the same local frame, so no slot is
|
|
327
|
+
// returned for a day the window cannot show.
|
|
140
328
|
var from = new Date()
|
|
141
|
-
var to =
|
|
329
|
+
var to = startOfLocalDay(from)
|
|
330
|
+
to.setDate(to.getDate() + daysAhead)
|
|
142
331
|
var url = apiBase + '/api/calendar/free-busy?from=' + encodeURIComponent(from.toISOString()) + '&to=' + encodeURIComponent(to.toISOString())
|
|
143
332
|
fetch(url)
|
|
144
333
|
.then(function (res) {
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
<!-- The calendar-site skill rewrites the content of this tag to the brand
|
|
7
7
|
tunnel origin that serves /api/calendar/free-busy. -->
|
|
8
8
|
<meta name="api-base" content="__API_BASE__" />
|
|
9
|
+
<!-- Operator-set booking horizon (positive integer days). The skill rewrites
|
|
10
|
+
the content; booking.js falls back to 14 when it is left unsubstituted. -->
|
|
11
|
+
<meta name="days-ahead" content="__DAYS_AHEAD__" />
|
|
12
|
+
<!-- Operator choice: may a visitor combine consecutive open slots into one
|
|
13
|
+
longer booking? "true" or "false"; booking.js falls back to false. -->
|
|
14
|
+
<meta name="allow-multi-slot" content="__ALLOW_MULTI_SLOT__" />
|
|
9
15
|
<title>Book a time</title>
|
|
10
16
|
<link rel="stylesheet" href="/booking.css" />
|
|
11
17
|
</head>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { LookupFunction } from "node:net";
|
|
2
|
+
import { fetch as undiciFetch, type Dispatcher } from "undici";
|
|
3
|
+
import { type Refusal, type Resolution } from "./ssrf.js";
|
|
2
4
|
import type { ConnectorRecord } from "./store.js";
|
|
3
5
|
export interface CallParams {
|
|
4
6
|
method: string;
|
|
@@ -8,10 +10,16 @@ export interface CallParams {
|
|
|
8
10
|
headers?: Record<string, string>;
|
|
9
11
|
}
|
|
10
12
|
export interface CallDeps {
|
|
11
|
-
/** Override for tests; defaults to
|
|
12
|
-
fetchImpl?: typeof
|
|
13
|
-
/** Override for tests so a loopback stub
|
|
14
|
-
|
|
13
|
+
/** Override for tests; defaults to undici's fetch (same instance as the pinned dispatcher). */
|
|
14
|
+
fetchImpl?: typeof undiciFetch;
|
|
15
|
+
/** Override for tests so a loopback stub resolves; defaults to the real address-validating resolve. */
|
|
16
|
+
resolve?: (targetHost: string, registeredHost: string, kind: "request" | "redirect") => Promise<Resolution>;
|
|
17
|
+
/**
|
|
18
|
+
* Build the dispatcher that pins the connection to the validated addresses. Defaults to
|
|
19
|
+
* the real pinned dispatcher. Tests return `undefined` to skip pinning (a pinned loopback
|
|
20
|
+
* address would otherwise fail the public re-validation).
|
|
21
|
+
*/
|
|
22
|
+
makeDispatcher?: (addresses: string[]) => Dispatcher | undefined;
|
|
15
23
|
/** Sink for `[connector]` lifecycle lines; the secret is never passed to it. */
|
|
16
24
|
log?: (line: string) => void;
|
|
17
25
|
reqId?: string;
|
|
@@ -27,10 +35,20 @@ export type CallResult = {
|
|
|
27
35
|
refused: Refusal;
|
|
28
36
|
};
|
|
29
37
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
38
|
+
* Build the DNS lookup an undici dispatcher uses to dial a connector. It returns the
|
|
39
|
+
* pre-validated addresses without consulting DNS — so no second, independent lookup
|
|
40
|
+
* happens at connect time — and re-validates each is public, failing closed. undici
|
|
41
|
+
* always calls it with `{ all: true }`; the single-result branch is kept for completeness.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pinnedLookup(addresses: string[]): LookupFunction;
|
|
44
|
+
/** Dispatcher that dials only the validated addresses; TLS/SNI still use the hostname, so cert validation is unchanged. */
|
|
45
|
+
export declare function pinnedDispatcher(addresses: string[]): Dispatcher;
|
|
46
|
+
/**
|
|
47
|
+
* Make one authenticated, host-bound request to a registered connector. The host is resolved
|
|
48
|
+
* and validated before the request and before every redirect hop, and the socket is pinned to
|
|
49
|
+
* the validated addresses so no second connect-time lookup can land on a rebind. The credential
|
|
50
|
+
* is injected from the caller-supplied secret and never logged. Returns status + redacted headers
|
|
51
|
+
* + capped body, or a refusal when resolution rejects the target.
|
|
34
52
|
*/
|
|
35
53
|
export declare function performCall(rec: ConnectorRecord, secret: string, params: CallParams, deps?: CallDeps): Promise<CallResult>;
|
|
36
54
|
//# sourceMappingURL=call.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/lib/call.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/lib/call.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAS,KAAK,IAAI,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EAA8B,KAAK,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,QAAQ;IACvB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,WAAW,CAAC;IAC/B,uGAAuG;IACvG,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5G;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,UAAU,GAAG,SAAS,CAAC;IACjE,gFAAgF;IAChF,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAC/F;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAQpC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,cAAc,CAUhE;AAED,2HAA2H;AAC3H,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,UAAU,CAEhE;AAkBD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,QAAa,GAClB,OAAO,CAAC,UAAU,CAAC,CA8FrB"}
|