@rubytech/create-maxy-code 0.1.352 → 0.1.353

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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/package-lock.json +76 -1
  3. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +2 -2
  4. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +41 -0
  5. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +236 -47
  6. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -0
  7. package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts +27 -9
  8. package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts.map +1 -1
  9. package/payload/platform/plugins/connector/mcp/dist/lib/call.js +102 -29
  10. package/payload/platform/plugins/connector/mcp/dist/lib/call.js.map +1 -1
  11. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts +17 -6
  12. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts.map +1 -1
  13. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js +28 -10
  14. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js.map +1 -1
  15. package/payload/platform/plugins/connector/mcp/package.json +13 -3
  16. package/payload/server/public/assets/{AdminLoginScreens-lt72mYrz.js → AdminLoginScreens-CzdOQKyO.js} +1 -1
  17. package/payload/server/public/assets/{AdminShell-B-GeoG3j.js → AdminShell-CC-CuN6Y.js} +1 -1
  18. package/payload/server/public/assets/{Checkbox-DoGMXVpF.js → Checkbox-sQBc9xSy.js} +1 -1
  19. package/payload/server/public/assets/{OperatorConversations-CfG1EYyP.js → OperatorConversations-CA_2c3sp.js} +1 -1
  20. package/payload/server/public/assets/OperatorConversations-DPVKGsRS.css +1 -0
  21. package/payload/server/public/assets/{admin-DzLxv-nz.js → admin-qW6jM6l0.js} +1 -1
  22. package/payload/server/public/assets/{browser-Cyi6v8BS.js → browser-BsW6KaxZ.js} +1 -1
  23. package/payload/server/public/assets/calendar-BPAiN1cL.js +1 -0
  24. package/payload/server/public/assets/chat-DVD9yoVL.js +1 -0
  25. package/payload/server/public/assets/{data-CrJZjlQ1.js → data-D_cR_NCZ.js} +1 -1
  26. package/payload/server/public/assets/{graph-CutaO0w1.js → graph-CmpAy2WP.js} +1 -1
  27. package/payload/server/public/assets/{graph-labels-CH0sWvcA.js → graph-labels-C0SLq_VP.js} +1 -1
  28. package/payload/server/public/assets/{operator-MASl7IqX.js → operator-CrjtJcEY.js} +1 -1
  29. package/payload/server/public/assets/page-Cf3fVDSo.js +32 -0
  30. package/payload/server/public/assets/{public-77Ubicd7.js → public-BkNtvSYp.js} +1 -1
  31. package/payload/server/public/browser.html +4 -4
  32. package/payload/server/public/calendar.html +4 -4
  33. package/payload/server/public/chat.html +5 -5
  34. package/payload/server/public/data.html +4 -4
  35. package/payload/server/public/graph.html +6 -6
  36. package/payload/server/public/index.html +6 -6
  37. package/payload/server/public/operator.html +7 -7
  38. package/payload/server/public/public.html +5 -5
  39. package/payload/server/server.js +125 -84
  40. package/payload/server/public/assets/OperatorConversations-C8G6Gfgu.css +0 -1
  41. package/payload/server/public/assets/calendar-CA3_Dm8j.js +0 -1
  42. package/payload/server/public/assets/chat-B9aNK1FG.js +0 -1
  43. package/payload/server/public/assets/page-CZy33qUH.js +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.352",
3
+ "version": "0.1.353",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -93,6 +93,10 @@
93
93
  "resolved": "plugins/cloudflare/mcp",
94
94
  "link": true
95
95
  },
96
+ "node_modules/@maxy/connector": {
97
+ "resolved": "plugins/connector/mcp",
98
+ "link": true
99
+ },
96
100
  "node_modules/@maxy/contacts": {
97
101
  "resolved": "plugins/contacts/mcp",
98
102
  "link": true
@@ -105,6 +109,10 @@
105
109
  "resolved": "plugins/graph-viewer/mcp",
106
110
  "link": true
107
111
  },
112
+ "node_modules/@maxy/joblogic": {
113
+ "resolved": "plugins/joblogic/mcp",
114
+ "link": true
115
+ },
108
116
  "node_modules/@maxy/memory": {
109
117
  "resolved": "plugins/memory/mcp",
110
118
  "link": true
@@ -117,6 +125,10 @@
117
125
  "resolved": "plugins/outlook/mcp",
118
126
  "link": true
119
127
  },
128
+ "node_modules/@maxy/quickbooks": {
129
+ "resolved": "plugins/quickbooks/mcp",
130
+ "link": true
131
+ },
120
132
  "node_modules/@maxy/replicate": {
121
133
  "resolved": "plugins/replicate/mcp",
122
134
  "link": true
@@ -125,6 +137,10 @@
125
137
  "resolved": "plugins/scheduling/mcp",
126
138
  "link": true
127
139
  },
140
+ "node_modules/@maxy/telegram-channel": {
141
+ "resolved": "services/telegram-channel",
142
+ "link": true
143
+ },
128
144
  "node_modules/@maxy/url-get": {
129
145
  "resolved": "plugins/url-get/mcp",
130
146
  "link": true
@@ -3494,6 +3510,15 @@
3494
3510
  "node": ">=14.17"
3495
3511
  }
3496
3512
  },
3513
+ "node_modules/undici": {
3514
+ "version": "6.27.0",
3515
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
3516
+ "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
3517
+ "license": "MIT",
3518
+ "engines": {
3519
+ "node": ">=18.17"
3520
+ }
3521
+ },
3497
3522
  "node_modules/undici-types": {
3498
3523
  "version": "6.21.0",
3499
3524
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -3856,6 +3881,19 @@
3856
3881
  "typescript": "^5.7.0"
3857
3882
  }
3858
3883
  },
3884
+ "plugins/connector/mcp": {
3885
+ "name": "@maxy/connector",
3886
+ "version": "0.1.0",
3887
+ "dependencies": {
3888
+ "@modelcontextprotocol/sdk": "^1.12.1",
3889
+ "undici": "^6.27.0",
3890
+ "zod": "^3.24.0"
3891
+ },
3892
+ "devDependencies": {
3893
+ "@types/node": "^22.0.0",
3894
+ "typescript": "^5.7.0"
3895
+ }
3896
+ },
3859
3897
  "plugins/contacts/mcp": {
3860
3898
  "name": "@maxy/contacts",
3861
3899
  "version": "0.1.0",
@@ -3865,7 +3903,8 @@
3865
3903
  },
3866
3904
  "devDependencies": {
3867
3905
  "@types/node": "^22.0.0",
3868
- "typescript": "^5.7.0"
3906
+ "typescript": "^5.7.0",
3907
+ "vitest": "^4.1.2"
3869
3908
  }
3870
3909
  },
3871
3910
  "plugins/email/mcp": {
@@ -3901,6 +3940,18 @@
3901
3940
  "vitest": "^4.1.2"
3902
3941
  }
3903
3942
  },
3943
+ "plugins/joblogic/mcp": {
3944
+ "name": "@maxy/joblogic",
3945
+ "version": "0.1.0",
3946
+ "dependencies": {
3947
+ "@modelcontextprotocol/sdk": "^1.12.1",
3948
+ "zod": "^3.24.0"
3949
+ },
3950
+ "devDependencies": {
3951
+ "@types/node": "^22.0.0",
3952
+ "typescript": "^5.7.0"
3953
+ }
3954
+ },
3904
3955
  "plugins/memory/mcp": {
3905
3956
  "name": "@maxy/memory",
3906
3957
  "version": "0.1.0",
@@ -3927,6 +3978,18 @@
3927
3978
  "typescript": "^5.7.0"
3928
3979
  }
3929
3980
  },
3981
+ "plugins/quickbooks/mcp": {
3982
+ "name": "@maxy/quickbooks",
3983
+ "version": "0.1.0",
3984
+ "dependencies": {
3985
+ "@modelcontextprotocol/sdk": "^1.12.1",
3986
+ "zod": "^3.24.0"
3987
+ },
3988
+ "devDependencies": {
3989
+ "@types/node": "^22.0.0",
3990
+ "typescript": "^5.7.0"
3991
+ }
3992
+ },
3930
3993
  "plugins/replicate/mcp": {
3931
3994
  "name": "@maxy/replicate",
3932
3995
  "version": "0.1.0",
@@ -4065,6 +4128,18 @@
4065
4128
  "vitest": "^4.1.2"
4066
4129
  }
4067
4130
  },
4131
+ "services/telegram-channel": {
4132
+ "name": "@maxy/telegram-channel",
4133
+ "version": "0.1.0",
4134
+ "dependencies": {
4135
+ "@modelcontextprotocol/sdk": "^1.0.0"
4136
+ },
4137
+ "devDependencies": {
4138
+ "@types/node": "^22.0.0",
4139
+ "typescript": "^5.7.0",
4140
+ "vitest": "^4.1.2"
4141
+ }
4142
+ },
4068
4143
  "services/webchat-channel": {
4069
4144
  "name": "@maxy/webchat-channel",
4070
4145
  "version": "0.1.0",
@@ -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`. Two substitutions make it concrete: set the `<meta name="api-base">` content in `public/index.html` to the `apiBase` value (the booking page fetches free/busy cross-origin from the brand tunnel), 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.
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
 
@@ -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. Let the visitor pick one, then collect name/email/note.
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 DAYS_AHEAD = 14
9
- var apiBaseMeta = document.querySelector('meta[name="api-base"]')
10
- var apiBase = (apiBaseMeta && apiBaseMeta.getAttribute('content')) || ''
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
- var chosenSlot = null
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 two weeks. Please check back later.')
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
- var byDay = {}
46
- var order = []
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 start = new Date(s.start)
49
- var key = start.toDateString()
50
- if (!byDay[key]) {
51
- byDay[key] = []
52
- order.push(key)
53
- }
54
- byDay[key].push(s)
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
- order.forEach(function (key) {
58
- var group = document.createElement('div')
59
- group.className = 'bk-day'
60
- var h = document.createElement('h2')
61
- h.className = 'bk-day-title'
62
- h.textContent = fmtDayHeading(new Date(key))
63
- group.appendChild(h)
64
-
65
- var row = document.createElement('div')
66
- row.className = 'bk-day-slots'
67
- byDay[key].forEach(function (s) {
68
- var btn = document.createElement('button')
69
- btn.type = 'button'
70
- btn.className = 'bk-slot'
71
- btn.textContent = fmtTime(new Date(s.start))
72
- btn.addEventListener('click', function () {
73
- chooseSlot(s)
74
- })
75
- row.appendChild(btn)
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
- group.appendChild(row)
78
- slotsEl.appendChild(group)
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(slot) {
83
- chosenSlot = slot
84
- var start = new Date(slot.start)
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
- chosenSlot = null
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 (!chosenSlot) return
282
+ if (!block) return
99
283
  var fd = new FormData(formEl)
100
284
  var payload = {
101
- slotStart: chosenSlot.start,
102
- slotEnd: chosenSlot.end,
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
- var start = new Date(chosenSlot.start)
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 = new Date(from.getTime() + DAYS_AHEAD * 24 * 60 * 60 * 1000)
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 { type Refusal } from "./ssrf.js";
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 global fetch. */
12
- fetchImpl?: typeof fetch;
13
- /** Override for tests so a loopback stub is reachable; defaults to the real SSRF guard. */
14
- guard?: (targetHost: string, registeredHost: string, kind: "request" | "redirect") => Promise<Refusal | null>;
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
- * Make one authenticated, host-bound request to a registered connector. The guard runs
31
- * before the request and before every redirect hop; the credential is injected from the
32
- * caller-supplied secret and never logged. Returns status + redacted headers + capped body,
33
- * or a refusal when the guard rejects the target.
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,EAAe,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AACtD,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,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,UAAU,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC9G,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;AAapC;;;;;GAKG;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,CA6DrB"}
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"}