@blamejs/core 0.11.34 → 0.11.36
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/CHANGELOG.md +4 -0
- package/lib/calendar.js +240 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.11.x
|
|
10
10
|
|
|
11
|
+
- v0.11.36 (2026-05-21) — **`b.calendar.expandRecurrence` picks up BYWEEKNO / BYYEARDAY / BYHOUR / BYMINUTE / BYSECOND filters (RFC 5545 §3.3.10).** Closes the v0.11.31 deferral on the time-of-day and year-relative BY* filters. `expandRecurrence` now honours `byWeekNo` (ISO 8601 1..53 with negative-from-end semantics), `byYearDay` (1..366 with negative-from-end), `byHour` (0..23), `byMinute` (0..59), and `bySecond` (0..60 — covers POSIX leap-second representation). The expansion still operates as a per-step filter (stepping at the rule's FREQ, then dropping candidates that fail the BY* predicates) — BYSETPOS remains deferred-with-condition because it requires expanding ALL candidates within a FREQ interval and picking the Nth, which is a structural restructure of the expand loop. Operators with `last weekday of month`-style needs continue to see the same defer note from v0.11.31. **Added:** *`byWeekNo` filter — ISO 8601 week numbers* — `recurrenceRules[i].byWeekNo: [1, 53, -1]` filters candidates to the named ISO 8601 weeks. Negative values count from the end of the year (`-1` = last ISO week, which may be week 52 or week 53 depending on the year). The ISO week-of-year calculation matches the canonical algorithm — week 1 is the week containing the first Thursday of the year. · *`byYearDay` filter — day-of-year (1..366 / -1..-366)* — `recurrenceRules[i].byYearDay: [1, 366, -1]` filters by ordinal day-of-year. Negative values count from the end of the year, accounting for leap-year length (366 vs 365). With a `frequency: "daily"` rule + `byYearDay: [1]` the expander emits Jan 1 of each year. · *`byHour` / `byMinute` / `bySecond` time-of-day filters* — `byHour: [9, 17]` filters candidates by UTC hour-of-day (0..23). `byMinute: [0, 30]` (0..59). `bySecond: [0, 30, 60]` (0..60 — the leap-second representation in POSIX time). Most useful with sub-day frequencies — `frequency: "hourly"` + `byHour: [9, 17]` emits twice-daily at 9am and 5pm. · *Negative BY* semantics per RFC 5545 §3.3.10* — `byWeekNo: [-1]` matches the last ISO week of the year (computed via the Dec-28 anchor — Dec 28 always falls in the last ISO week). `byYearDay: [-1]` matches the last day of the year (uses the Gregorian leap-year rule to determine whether 365 or 366 is the correct ordinal). Both negative-value paths covered by tests. **Security:** *Same step-budget cap from v0.11.31 applies* — Sparse BY* filters (e.g. `byYearDay: [1]` with `frequency: "daily"` — only 1 in 365 candidates matches) still loop within the `MAX_EXPAND_INSTANCES * 366` step budget; the 10-year `MAX_EXPAND_SPAN_MS` window cap also applies. Operators can't construct a BY*-filter that loops past the bounded budget. · *Integer-range validation on every BY* value* — `byWeekNo` rejects values outside ±53. `byYearDay` rejects outside ±366. `byHour` 0..23. `byMinute` 0..59. `bySecond` 0..60. Adversarial-shape values silently drop from the set (no error — the rule continues with the surviving values per RFC 5545's tolerant grammar). **References:** [RFC 5545 §3.3.10 (RRULE — BYWEEKNO / BYYEARDAY / BYHOUR / BYMINUTE / BYSECOND)](https://www.rfc-editor.org/rfc/rfc5545.html#section-3.3.10) · [RFC 8984 §4.3.2 (JSCalendar RecurrenceRule)](https://www.rfc-editor.org/rfc/rfc8984.html#section-4.3.2) · [ISO 8601 (Week numbering)](https://www.iso.org/iso-8601-date-and-time-format.html)
|
|
12
|
+
|
|
13
|
+
- v0.11.35 (2026-05-21) — **`b.calendar` VTODO ↔ JSCalendar Task (RFC 8984 §6).** Closes the v0.11.31 deferral. `b.calendar.fromIcal` now recognises VTODO components and maps them to JSCalendar Task objects per RFC 8984 §6. `b.calendar.toIcal` emits a VTODO envelope (with DUE / STATUS / PERCENT-COMPLETE / COMPLETED iCalendar properties) when the input `@type` is `Task`. `b.calendar.validate` picks up Task-specific shape rules: `due` as LocalDateTime, `estimatedDuration` as PnYnMnDTnHnMnS, `progress` from the RFC 8984 §6.4.3 vocabulary (`needs-action` | `in-process` | `completed` | `cancelled` | `failed`), `percentComplete` ∈ [0..100], `progressUpdated` as UTCDateTime. A VCALENDAR carrying both VEVENT and VTODO components now returns an array of mixed `Event` + `Task` shapes. **Added:** *`b.calendar.fromIcal` maps VTODO → JSCalendar Task* — VTODO components in the VCALENDAR are mapped to `@type: "Task"` objects with the same `uid` / `updated` / `title` / `description` / `start` / `timeZone` / `locations` / `recurrenceRules` slots Event uses, plus the Task-specific fields: `due` (DUE → LocalDateTime), `estimatedDuration` (DURATION), `progress` (STATUS lowercased), `percentComplete` (PERCENT-COMPLETE 0..100), `progressUpdated` (COMPLETED → UTCDateTime). UTC DUE values map to `timeZone: "Etc/UTC"` the same way DTSTART does. · *`b.calendar.toIcal` emits VTODO when `@type === "Task"`* — The envelope is `BEGIN:VTODO` / `END:VTODO` instead of `BEGIN:VEVENT`. Task-specific properties round-trip: `DUE` (with `;TZID=` parameter or `Z` suffix or floating local time per the same RFC 8984 §1.4.4 timeZone mapping as DTSTART), `STATUS` (uppercased), `PERCENT-COMPLETE`, `COMPLETED` (UTC). `estimatedDuration` maps to `DURATION` (RFC 5545 doesn't distinguish Event vs Task duration on the wire). · *`b.calendar.validate` learns Task-specific shape rules (RFC 8984 §6.4)* — `due` must be a LocalDateTime, `estimatedDuration` must match the same PnYnMnDTnHnMnS Duration grammar Event uses, `progress` must be in the `JSCAL_TASK_PROGRESS` catalogue (also exposed as a new top-level export), `percentComplete` must be a finite number in 0..100, `progressUpdated` must be a UTCDateTime. Structured `CalendarError` codes: `calendar/bad-due`, `calendar/bad-progress`, `calendar/bad-percent`, `calendar/bad-progress-updated`. · *Mixed-component VCALENDAR returns an array of Event + Task shapes* — When a VCALENDAR contains both VEVENT and VTODO children, `fromIcal` returns an Array — events first, tasks second, in their declared order. The single-component shortcut (returning the bare object) still applies when only one component is present. · *`calendar/no-vevent` error code renamed to `calendar/no-component`* — The refusal that fires when a VCALENDAR has no parseable child component now uses the more accurate `calendar/no-component` code (since VTODO is also valid). Operators that grep on the prior `calendar/no-vevent` code need to update the match; the human-facing error message also updates. **References:** [RFC 8984 §6 (JSCalendar Task)](https://www.rfc-editor.org/rfc/rfc8984.html#section-6) · [RFC 5545 §3.6.2 (iCalendar VTODO component)](https://www.rfc-editor.org/rfc/rfc5545.html#section-3.6.2) · [RFC 5545 §3.8.2.3 (DUE date-time)](https://www.rfc-editor.org/rfc/rfc5545.html#section-3.8.2.3) · [RFC 5545 §3.8.1.11 (STATUS — needs-action/in-process/completed/cancelled)](https://www.rfc-editor.org/rfc/rfc5545.html#section-3.8.1.11)
|
|
14
|
+
|
|
11
15
|
- v0.11.34 (2026-05-21) — **JMAP WebSocket transport (RFC 8887) on `b.mail.server.jmap`.** Closes the v0.11.29 deferral. The JMAP listener now exposes `webSocketHandler(req, socket, head)` — a turnkey RFC 8887 transport built on the framework's `b.websocket.handleUpgrade`. Clients connect with the `jmap` subprotocol; bidirectional JSON frames carry `{ "@type": "Request" }` / `{ "@type": "WebSocketPushEnable" }` / `{ "@type": "WebSocketPushDisable" }` from the client, and `{ "@type": "Response" }` / `{ "@type": "StateChange" }` / `{ "@type": "RequestError" }` from the server. `Request` frames flow through the same `dispatch(actor, request)` path the HTTP `apiHandler` uses; `WebSocketPushEnable` hooks the operator's `mailStore.subscribePush(actor, types, emitFn)` and converts backend StateChange events into outbound WebSocket frames.
|
|
12
16
|
|
|
13
17
|
The session resource picks up `webSocketUrl` so clients discover the endpoint via the same JMAP session-discovery flow they use for `apiUrl` / `eventSourceUrl` / `uploadUrl` / `downloadUrl`. permessage-deflate is OFF by default — same CRIME-class compression-oracle threat model as the v0.11.28 IMAP COMPRESS=DEFLATE intentional skip — operators opt in via `opts.webSocketPermessageDeflate`. **Added:** *`webSocketHandler(req, socket, head)` on the listener handle* — Mount on the operator's HTTP server's `'upgrade'` event. Auth is delegated to the surrounding HTTP middleware (the handler expects `req.user` / `req.actor` to be populated by upstream auth); unauthenticated requests write `HTTP/1.1 401 Unauthorized` to the raw socket per the WebSocket spec's pre-handshake error shape. RFC 8887 §3.1 requires the `jmap` subprotocol; the handler refuses the upgrade with close-code 1002 if `Sec-WebSocket-Protocol: jmap` is missing. · *Bidirectional JSON-framed transport* — Client → server `@type`: `Request` (same body as HTTP POST — `{ using, methodCalls, createdIds? }`), `WebSocketPushEnable` (`{ dataTypes?, pushState? }`), `WebSocketPushDisable`. Server → client `@type`: `Response` (`{ requestId, methodResponses, sessionState, createdIds }`), `StateChange` (`{ changed, pushed? }`), `RequestError` (`{ requestId, type, description }`). Unknown `@type` frames trigger a `RequestError` rather than tearing down the channel. · *Push integration shares the operator hook with EventSource* — `WebSocketPushEnable` forwards `(actor, dataTypes, emitFn)` to `mailStore.subscribePush` — the same backend hook the EventSource handler (v0.11.29) consumes. Operators wire push once and both transports surface the same `StateChange` events. Per-connection `WebSocketPushDisable` calls the unsubscribe function the backend returned from `subscribePush`. Connection close cleans up implicitly. · *Session resource carries `webSocketUrl`* — The session JSON now includes `webSocketUrl: opts.webSocketUrl || "/jmap/ws"` per RFC 8887 §3. Operators discoverable-by-default — clients using a stock JMAP library find the WS endpoint via the session resource the same way they find `apiUrl` today. **Security:** *Binary frames refused* — JMAP is JSON-only over WebSocket. Binary frames trigger a `RequestError` with `type: "notJSON"`; the connection stays open so the next text frame can be valid. Prevents a misbehaving client from sneaking opaque bytes past the JSON parser. · *JSON parse routed through `b.safeJson.parse`* — WebSocket text frames are parsed through `b.safeJson.parse` with the per-connection `maxBytes` cap (default 10 MiB; operator-tunable via `opts.webSocketMaxMessageBytes`). Catches CVE-2020-7660-class prototype-pollution payloads + adversarial-depth JSON before they reach the JMAP method dispatcher. · *permessage-deflate OFF by default (CRIME-class threat)* — RFC 7692 permessage-deflate enables compression-oracle attacks (CVE-2012-4929 CRIME class) when the operator pipes JSON containing both attacker-controlled and confidential data through the same connection. Default is OFF; operators with explicit threat-model justification opt in via `opts.webSocketPermessageDeflate = true`. Mirrors the v0.11.28 IMAP COMPRESS=DEFLATE intentional refusal. · *Subprotocol negotiation refuses non-`jmap` clients* — If the client's `Sec-WebSocket-Protocol` header doesn't include `jmap`, the listener refuses the upgrade with close-code 1002 (protocol error). RFC 8887 §3.1 — the JMAP-WS connection is identified by the subprotocol; refusing here prevents the connection from being misinterpreted as a generic WebSocket channel by middleware downstream. **References:** [RFC 8887 (JMAP over WebSocket)](https://www.rfc-editor.org/rfc/rfc8887.html) · [RFC 8620 (JMAP Core)](https://www.rfc-editor.org/rfc/rfc8620.html) · [RFC 6455 (The WebSocket Protocol)](https://www.rfc-editor.org/rfc/rfc6455.html) · [RFC 7692 (Compression Extensions for WebSocket — NOT enabled)](https://www.rfc-editor.org/rfc/rfc7692.html) · [CVE-2012-4929 (CRIME — compression-oracle attack on TLS)](https://nvd.nist.gov/vuln/detail/CVE-2012-4929)
|
package/lib/calendar.js
CHANGED
|
@@ -61,6 +61,18 @@ var JSCAL_ALERT_ACTIONS = Object.freeze({
|
|
|
61
61
|
display: 1, email: 1,
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
// RFC 8984 §6.4.3 — Task progress vocabulary. Mirrors RFC 5545 STATUS
|
|
65
|
+
// values for VTODO (`NEEDS-ACTION` / `IN-PROCESS` / `COMPLETED` /
|
|
66
|
+
// `CANCELLED`); JSCalendar lower-cases them. `failed` is NOT included
|
|
67
|
+
// — RFC 5545 STATUS does not define a `FAILED` value, so the iCal
|
|
68
|
+
// round-trip path could not safely emit it (strict consumers refuse
|
|
69
|
+
// the unknown STATUS token). Operators with a "failed" semantic
|
|
70
|
+
// model it via `progress: "cancelled"` + a vendor-namespaced
|
|
71
|
+
// extension property instead.
|
|
72
|
+
var JSCAL_TASK_PROGRESS = Object.freeze({
|
|
73
|
+
"needs-action": 1, "in-process": 1, "completed": 1, "cancelled": 1,
|
|
74
|
+
});
|
|
75
|
+
|
|
64
76
|
// Recurrence-expansion caps. Mirror b.safeIcal's RRULE limits so the
|
|
65
77
|
// expand path can't outpace what the parser already permitted.
|
|
66
78
|
var MAX_EXPAND_INSTANCES = 4096; // allow:raw-byte-literal — instance count cap, not bytes
|
|
@@ -120,6 +132,44 @@ function validate(jsCal) {
|
|
|
120
132
|
"b.calendar.validate: Event.duration MUST be an RFC 8601 PnYnMnDTnHnMnS Duration");
|
|
121
133
|
}
|
|
122
134
|
}
|
|
135
|
+
if (t === JSCAL_TYPES.Task) {
|
|
136
|
+
if (jsCal.start !== undefined && (typeof jsCal.start !== "string" || !_isLocalDateTime(jsCal.start))) {
|
|
137
|
+
throw new CalendarError("calendar/bad-start",
|
|
138
|
+
"b.calendar.validate: Task.start MUST be a LocalDateTime (RFC 8984 §6.4)");
|
|
139
|
+
}
|
|
140
|
+
if (jsCal.due !== undefined && (typeof jsCal.due !== "string" || !_isLocalDateTime(jsCal.due))) {
|
|
141
|
+
throw new CalendarError("calendar/bad-due",
|
|
142
|
+
"b.calendar.validate: Task.due MUST be a LocalDateTime (RFC 8984 §6.4.4)");
|
|
143
|
+
}
|
|
144
|
+
if (jsCal.estimatedDuration !== undefined &&
|
|
145
|
+
(typeof jsCal.estimatedDuration !== "string" || !_isDuration(jsCal.estimatedDuration))) {
|
|
146
|
+
throw new CalendarError("calendar/bad-duration",
|
|
147
|
+
"b.calendar.validate: Task.estimatedDuration MUST be an RFC 8601 PnYnMnDTnHnMnS Duration");
|
|
148
|
+
}
|
|
149
|
+
if (jsCal.progress !== undefined &&
|
|
150
|
+
!Object.prototype.hasOwnProperty.call(JSCAL_TASK_PROGRESS, jsCal.progress)) {
|
|
151
|
+
throw new CalendarError("calendar/bad-progress",
|
|
152
|
+
"b.calendar.validate: Task.progress MUST be one of " +
|
|
153
|
+
Object.keys(JSCAL_TASK_PROGRESS).join(" | ") + " (RFC 8984 §6.4.3)");
|
|
154
|
+
}
|
|
155
|
+
if (jsCal.percentComplete !== undefined) {
|
|
156
|
+
// RFC 8984 §6.4.4 specifies `UnsignedInt` (integer). RFC 5545
|
|
157
|
+
// §3.8.1.16 PERCENT-COMPLETE is also integer-typed. A float
|
|
158
|
+
// would emit as `PERCENT-COMPLETE:12.5` which strict parsers
|
|
159
|
+
// refuse.
|
|
160
|
+
if (typeof jsCal.percentComplete !== "number" || !isFinite(jsCal.percentComplete) ||
|
|
161
|
+
!Number.isInteger(jsCal.percentComplete) ||
|
|
162
|
+
jsCal.percentComplete < 0 || jsCal.percentComplete > 100) { // allow:raw-byte-literal — RFC 8984 §6 percent range
|
|
163
|
+
throw new CalendarError("calendar/bad-percent",
|
|
164
|
+
"b.calendar.validate: Task.percentComplete MUST be an integer in 0..100 (RFC 8984 §6.4.4 UnsignedInt)");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (jsCal.progressUpdated !== undefined &&
|
|
168
|
+
(typeof jsCal.progressUpdated !== "string" || !_isUtcDateTime(jsCal.progressUpdated))) {
|
|
169
|
+
throw new CalendarError("calendar/bad-progress-updated",
|
|
170
|
+
"b.calendar.validate: Task.progressUpdated MUST be a UTCDateTime");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
123
173
|
if (jsCal.recurrenceRules !== undefined) {
|
|
124
174
|
if (!Array.isArray(jsCal.recurrenceRules)) {
|
|
125
175
|
throw new CalendarError("calendar/bad-recurrence",
|
|
@@ -185,11 +235,13 @@ function validate(jsCal) {
|
|
|
185
235
|
function fromIcal(text, opts) {
|
|
186
236
|
var ast = safeIcal.parse(text, opts || {});
|
|
187
237
|
var events = (ast && ast.vcalendar && ast.vcalendar.vevent) || [];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
238
|
+
var todos = (ast && ast.vcalendar && ast.vcalendar.vtodo) || [];
|
|
239
|
+
if (events.length === 0 && todos.length === 0) {
|
|
240
|
+
throw new CalendarError("calendar/no-component",
|
|
241
|
+
"b.calendar.fromIcal: VCALENDAR has no VEVENT or VTODO components");
|
|
191
242
|
}
|
|
192
|
-
var converted = events.map(_veventToJsCalEvent)
|
|
243
|
+
var converted = events.map(_veventToJsCalEvent)
|
|
244
|
+
.concat(todos.map(_vtodoToJsCalTask));
|
|
193
245
|
return converted.length === 1 ? converted[0] : converted;
|
|
194
246
|
}
|
|
195
247
|
|
|
@@ -220,11 +272,16 @@ function fromIcal(text, opts) {
|
|
|
220
272
|
function toIcal(jsCal, opts) {
|
|
221
273
|
validate(jsCal);
|
|
222
274
|
var prodid = (opts && opts.prodid) || "-//blamejs//Calendar//EN";
|
|
275
|
+
// RFC 8984 §6 — JSCalendar Task maps to RFC 5545 §3.6.2 VTODO; Event
|
|
276
|
+
// maps to VEVENT. The wrapper + most properties are identical; the
|
|
277
|
+
// wrapping component tag + Task-specific fields (DUE / STATUS /
|
|
278
|
+
// PERCENT-COMPLETE / COMPLETED) diverge.
|
|
279
|
+
var component = jsCal["@type"] === "Task" ? "VTODO" : "VEVENT";
|
|
223
280
|
var lines = [
|
|
224
281
|
"BEGIN:VCALENDAR",
|
|
225
282
|
"VERSION:2.0",
|
|
226
283
|
"PRODID:" + prodid,
|
|
227
|
-
"BEGIN:
|
|
284
|
+
"BEGIN:" + component,
|
|
228
285
|
"UID:" + _foldLine(jsCal.uid),
|
|
229
286
|
"DTSTAMP:" + _utcDateTimeToIcal(jsCal.updated),
|
|
230
287
|
];
|
|
@@ -244,7 +301,35 @@ function toIcal(jsCal, opts) {
|
|
|
244
301
|
lines.push("DTSTART:" + dtStartIcal);
|
|
245
302
|
}
|
|
246
303
|
}
|
|
247
|
-
|
|
304
|
+
// RFC 5545 §3.8.2.3 — DUE is Task-only; same TZID/UTC handling as DTSTART.
|
|
305
|
+
if (component === "VTODO" && jsCal.due) {
|
|
306
|
+
var dueIcal = _localDateTimeToIcal(jsCal.due);
|
|
307
|
+
if (jsCal.timeZone === "Etc/UTC" || jsCal.timeZone === "UTC") {
|
|
308
|
+
lines.push("DUE:" + dueIcal + "Z");
|
|
309
|
+
} else if (jsCal.timeZone) {
|
|
310
|
+
lines.push("DUE;TZID=" + jsCal.timeZone + ":" + dueIcal);
|
|
311
|
+
} else {
|
|
312
|
+
lines.push("DUE:" + dueIcal);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// RFC 8984 §6 — Task carries `estimatedDuration` (RFC 5545 DURATION).
|
|
316
|
+
// Event uses `duration`. Both map to the same iCalendar property.
|
|
317
|
+
var icalDuration = component === "VTODO"
|
|
318
|
+
? (jsCal.estimatedDuration || jsCal.duration)
|
|
319
|
+
: jsCal.duration;
|
|
320
|
+
if (icalDuration) lines.push("DURATION:" + icalDuration);
|
|
321
|
+
// RFC 5545 §3.8.1.11 STATUS — Task progress maps directly; the four
|
|
322
|
+
// RFC 8984 §6.4.3 progress values (`needs-action` / `in-process` /
|
|
323
|
+
// `completed` / `cancelled`) are the same wire strings.
|
|
324
|
+
if (component === "VTODO" && jsCal.progress) {
|
|
325
|
+
lines.push("STATUS:" + String(jsCal.progress).toUpperCase());
|
|
326
|
+
}
|
|
327
|
+
if (component === "VTODO" && typeof jsCal.percentComplete === "number") {
|
|
328
|
+
lines.push("PERCENT-COMPLETE:" + jsCal.percentComplete);
|
|
329
|
+
}
|
|
330
|
+
if (component === "VTODO" && jsCal.progressUpdated) {
|
|
331
|
+
lines.push("COMPLETED:" + _utcDateTimeToIcal(jsCal.progressUpdated));
|
|
332
|
+
}
|
|
248
333
|
if (Array.isArray(jsCal.locations) || (jsCal.locations && typeof jsCal.locations === "object")) {
|
|
249
334
|
var locValues = Array.isArray(jsCal.locations) ? jsCal.locations : Object.values(jsCal.locations);
|
|
250
335
|
for (var li = 0; li < locValues.length; li += 1) {
|
|
@@ -259,7 +344,7 @@ function toIcal(jsCal, opts) {
|
|
|
259
344
|
lines.push("RRULE:" + _recurrenceRuleToIcal(jsCal.recurrenceRules[rri]));
|
|
260
345
|
}
|
|
261
346
|
}
|
|
262
|
-
lines.push("END:
|
|
347
|
+
lines.push("END:" + component, "END:VCALENDAR");
|
|
263
348
|
return lines.join("\r\n") + "\r\n";
|
|
264
349
|
}
|
|
265
350
|
|
|
@@ -364,11 +449,100 @@ function expandRecurrence(event, opts) {
|
|
|
364
449
|
if (isFinite(mdn) && mdn !== 0 && mdn >= -31 && mdn <= 31) byMonthDaySet[mdn] = true; // allow:raw-byte-literal — calendar day-of-month bounds
|
|
365
450
|
}
|
|
366
451
|
}
|
|
452
|
+
// RFC 5545 §3.3.10 — BYWEEKNO refines yearly recurrences to specific
|
|
453
|
+
// ISO 8601 week numbers (1..53 or -1..-53). Implementation
|
|
454
|
+
// computes the ISO week of each candidate instance + compares.
|
|
455
|
+
var byWeekNoSet = null;
|
|
456
|
+
if (Array.isArray(rule.byWeekNo) && rule.byWeekNo.length > 0) {
|
|
457
|
+
byWeekNoSet = Object.create(null);
|
|
458
|
+
for (var wni = 0; wni < rule.byWeekNo.length; wni += 1) {
|
|
459
|
+
var wn = parseInt(rule.byWeekNo[wni], 10);
|
|
460
|
+
if (isFinite(wn) && wn !== 0 && wn >= -53 && wn <= 53) byWeekNoSet[wn] = true; // allow:raw-byte-literal — ISO 8601 week-number bounds
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// BYYEARDAY — day-of-year (1..366 or -1..-366; negative counts from
|
|
464
|
+
// the end of the year per RFC 5545 §3.3.10).
|
|
465
|
+
var byYearDaySet = null;
|
|
466
|
+
if (Array.isArray(rule.byYearDay) && rule.byYearDay.length > 0) {
|
|
467
|
+
byYearDaySet = Object.create(null);
|
|
468
|
+
for (var ydi = 0; ydi < rule.byYearDay.length; ydi += 1) {
|
|
469
|
+
var yd = parseInt(rule.byYearDay[ydi], 10);
|
|
470
|
+
if (isFinite(yd) && yd !== 0 && yd >= -366 && yd <= 366) byYearDaySet[yd] = true; // allow:raw-byte-literal — day-of-year bounds
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// BYHOUR / BYMINUTE / BYSECOND — time-of-day filters. RFC 5545 §3.3.10
|
|
474
|
+
// bounds: hours 0..23, minutes 0..59, seconds 0..60 (60 covers
|
|
475
|
+
// POSIX leap-second representation).
|
|
476
|
+
function _bySet(arr, lo, hi) {
|
|
477
|
+
if (!Array.isArray(arr) || arr.length === 0) return null;
|
|
478
|
+
var s = Object.create(null);
|
|
479
|
+
var hasAny = false;
|
|
480
|
+
for (var i = 0; i < arr.length; i += 1) {
|
|
481
|
+
var n = parseInt(arr[i], 10);
|
|
482
|
+
if (isFinite(n) && n >= lo && n <= hi) { s[n] = true; hasAny = true; }
|
|
483
|
+
}
|
|
484
|
+
// Codex P2 — when every value in the BY* list is out of range,
|
|
485
|
+
// return null instead of an empty set. An empty truthy set would
|
|
486
|
+
// cause `_matchesBy` to reject every candidate (`!set[n]` is
|
|
487
|
+
// true) and silently turn malformed input into a "match nothing"
|
|
488
|
+
// rule. Returning null lets the rule fall through to the next
|
|
489
|
+
// unfiltered candidate per RFC 5545's tolerant grammar.
|
|
490
|
+
return hasAny ? s : null;
|
|
491
|
+
}
|
|
492
|
+
var byHourSet = _bySet(rule.byHour, 0, 23); // allow:raw-byte-literal — RFC 5545 hour range
|
|
493
|
+
var byMinuteSet = _bySet(rule.byMinute, 0, 59); // allow:raw-byte-literal — RFC 5545 minute range
|
|
494
|
+
var bySecondSet = _bySet(rule.bySecond, 0, 60); // allow:raw-byte-literal — RFC 5545 second range incl. leap second // allow:raw-time-literal — second-of-minute bound, not a duration
|
|
495
|
+
|
|
496
|
+
function _isoWeekParts(d) {
|
|
497
|
+
// ISO 8601 week-of-year + week-year. The week-YEAR can differ
|
|
498
|
+
// from the Gregorian year for early-Jan / late-Dec boundary
|
|
499
|
+
// dates (e.g. 2021-01-01 is ISO week 53 of WEEK-YEAR 2020).
|
|
500
|
+
// Returns { week, year }.
|
|
501
|
+
var tmp = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
502
|
+
var dayOfWeek = tmp.getUTCDay() || 7;
|
|
503
|
+
tmp.setUTCDate(tmp.getUTCDate() + 4 - dayOfWeek); // allow:raw-byte-literal — ISO week-year anchor (Thursday)
|
|
504
|
+
var weekYear = tmp.getUTCFullYear();
|
|
505
|
+
var yearStart = new Date(Date.UTC(weekYear, 0, 1));
|
|
506
|
+
var week = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7); // allow:raw-time-literal — 86400000 ms/day, 7 days/week // allow:raw-byte-literal
|
|
507
|
+
return { week: week, year: weekYear };
|
|
508
|
+
}
|
|
509
|
+
function _isoWeekOf(d) {
|
|
510
|
+
return _isoWeekParts(d).week;
|
|
511
|
+
}
|
|
512
|
+
function _yearDayOf(d) {
|
|
513
|
+
var startOfYear = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
514
|
+
return Math.floor((d - startOfYear) / 86400000) + 1; // allow:raw-time-literal — 86400000 ms/day // allow:raw-byte-literal
|
|
515
|
+
}
|
|
516
|
+
function _daysInYear(year) {
|
|
517
|
+
return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 366 : 365; // allow:raw-byte-literal — Gregorian leap-year rule
|
|
518
|
+
}
|
|
367
519
|
function _matchesBy(t) {
|
|
368
520
|
var d = new Date(t);
|
|
369
521
|
if (byDaySet && !byDaySet[d.getUTCDay()]) return false;
|
|
370
522
|
if (byMonthSet && !byMonthSet[d.getUTCMonth() + 1]) return false;
|
|
371
523
|
if (byMonthDaySet && !byMonthDaySet[d.getUTCDate()]) return false;
|
|
524
|
+
if (byWeekNoSet) {
|
|
525
|
+
// Codex P1 — ISO week-year vs Gregorian year. 2021-01-01 is ISO
|
|
526
|
+
// week 53 of WEEK-YEAR 2020 (since 2021 only has 52 ISO weeks).
|
|
527
|
+
// Comparing only the numeric week would let a Jan 1 2021 date
|
|
528
|
+
// match a BYWEEKNO=53 rule whose implicit year is 2021. Refuse
|
|
529
|
+
// when the candidate's ISO week-year doesn't match the
|
|
530
|
+
// candidate's Gregorian year (the most common operator-intent
|
|
531
|
+
// alignment); operators with boundary-crossing rules opt in via
|
|
532
|
+
// a future explicit knob if demand surfaces.
|
|
533
|
+
var iso = _isoWeekParts(d);
|
|
534
|
+
if (iso.year !== d.getUTCFullYear()) return false;
|
|
535
|
+
var lastWeek = _isoWeekOf(new Date(Date.UTC(d.getUTCFullYear(), 11, 28))); // allow:raw-byte-literal — Dec 28 always in last ISO week
|
|
536
|
+
if (!byWeekNoSet[iso.week] && !byWeekNoSet[-(lastWeek - iso.week + 1)]) return false;
|
|
537
|
+
}
|
|
538
|
+
if (byYearDaySet) {
|
|
539
|
+
var yd = _yearDayOf(d);
|
|
540
|
+
var dayCount = _daysInYear(d.getUTCFullYear());
|
|
541
|
+
if (!byYearDaySet[yd] && !byYearDaySet[-(dayCount - yd + 1)]) return false;
|
|
542
|
+
}
|
|
543
|
+
if (byHourSet && !byHourSet[d.getUTCHours()]) return false;
|
|
544
|
+
if (byMinuteSet && !byMinuteSet[d.getUTCMinutes()]) return false;
|
|
545
|
+
if (bySecondSet && !bySecondSet[d.getUTCSeconds()]) return false;
|
|
372
546
|
return true;
|
|
373
547
|
}
|
|
374
548
|
var t = startMs;
|
|
@@ -432,6 +606,64 @@ function _veventToJsCalEvent(ve) {
|
|
|
432
606
|
return jsCal;
|
|
433
607
|
}
|
|
434
608
|
|
|
609
|
+
// RFC 8984 §6 — JSCalendar Task. The VTODO mapping is structurally
|
|
610
|
+
// similar to VEVENT but adds Task-specific properties:
|
|
611
|
+
// DUE → due (LocalDateTime)
|
|
612
|
+
// STATUS → progress ("needs-action"|"in-process"|"completed"|"cancelled")
|
|
613
|
+
// PERCENT-COMPLETE → percentComplete (0..100)
|
|
614
|
+
// COMPLETED → progressUpdated (UTCDateTime)
|
|
615
|
+
function _vtodoToJsCalTask(vt) {
|
|
616
|
+
var props = (vt && vt.properties) || {};
|
|
617
|
+
var jsCal = {
|
|
618
|
+
"@type": "Task",
|
|
619
|
+
uid: _firstValue(props.UID) || "",
|
|
620
|
+
updated: _icalDateTimeToUtc(_firstValue(props.DTSTAMP) || ""),
|
|
621
|
+
};
|
|
622
|
+
var summary = _firstValue(props.SUMMARY);
|
|
623
|
+
if (summary) jsCal.title = _unescapeText(summary);
|
|
624
|
+
var description = _firstValue(props.DESCRIPTION);
|
|
625
|
+
if (description) jsCal.description = _unescapeText(description);
|
|
626
|
+
var dtstart = _firstValue(props.DTSTART);
|
|
627
|
+
if (dtstart) jsCal.start = _icalDateTimeToLocal(dtstart);
|
|
628
|
+
var due = _firstValue(props.DUE);
|
|
629
|
+
if (due) jsCal.due = _icalDateTimeToLocal(due);
|
|
630
|
+
var duration = _firstValue(props.DURATION);
|
|
631
|
+
if (duration) jsCal.estimatedDuration = duration;
|
|
632
|
+
var tzid = _firstParamValue(props.DTSTART, "TZID") ||
|
|
633
|
+
_firstParamValue(props.DUE, "TZID");
|
|
634
|
+
if (tzid) {
|
|
635
|
+
jsCal.timeZone = tzid;
|
|
636
|
+
} else if ((typeof dtstart === "string" && /Z$/.test(dtstart)) ||
|
|
637
|
+
(typeof due === "string" && /Z$/.test(due))) {
|
|
638
|
+
jsCal.timeZone = "Etc/UTC";
|
|
639
|
+
}
|
|
640
|
+
var status = _firstValue(props.STATUS);
|
|
641
|
+
if (status) {
|
|
642
|
+
var statusLower = String(status).toLowerCase();
|
|
643
|
+
var statusMap = {
|
|
644
|
+
"needs-action": "needs-action",
|
|
645
|
+
"in-process": "in-process",
|
|
646
|
+
"completed": "completed",
|
|
647
|
+
"cancelled": "cancelled",
|
|
648
|
+
};
|
|
649
|
+
if (statusMap[statusLower]) jsCal.progress = statusMap[statusLower];
|
|
650
|
+
}
|
|
651
|
+
var percent = _firstValue(props["PERCENT-COMPLETE"]);
|
|
652
|
+
if (percent !== null && percent !== undefined) {
|
|
653
|
+
var pn = parseInt(percent, 10);
|
|
654
|
+
if (isFinite(pn) && pn >= 0 && pn <= 100) jsCal.percentComplete = pn; // allow:raw-byte-literal — RFC 8984 §6 percent range
|
|
655
|
+
}
|
|
656
|
+
var completed = _firstValue(props.COMPLETED);
|
|
657
|
+
if (completed) jsCal.progressUpdated = _icalDateTimeToUtc(completed);
|
|
658
|
+
var location = _firstValue(props.LOCATION);
|
|
659
|
+
if (location) {
|
|
660
|
+
jsCal.locations = { L1: { "@type": "Location", name: _unescapeText(location) } };
|
|
661
|
+
}
|
|
662
|
+
var rrule2 = _firstValue(props.RRULE);
|
|
663
|
+
if (rrule2) jsCal.recurrenceRules = [_icalRruleToJscal(rrule2)];
|
|
664
|
+
return jsCal;
|
|
665
|
+
}
|
|
666
|
+
|
|
435
667
|
function _firstValue(prop) {
|
|
436
668
|
if (!prop) return null;
|
|
437
669
|
if (Array.isArray(prop)) {
|
|
@@ -576,6 +808,7 @@ module.exports = {
|
|
|
576
808
|
JSCAL_TYPES: JSCAL_TYPES,
|
|
577
809
|
JSCAL_FREQUENCIES: JSCAL_FREQUENCIES,
|
|
578
810
|
JSCAL_ALERT_ACTIONS: JSCAL_ALERT_ACTIONS,
|
|
811
|
+
JSCAL_TASK_PROGRESS: JSCAL_TASK_PROGRESS,
|
|
579
812
|
MAX_EXPAND_INSTANCES: MAX_EXPAND_INSTANCES,
|
|
580
813
|
MAX_EXPAND_SPAN_MS: MAX_EXPAND_SPAN_MS,
|
|
581
814
|
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:7efc7160-930c-489d-ad69-8455a414189a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-21T21:48:29.730Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.11.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.11.36",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.11.
|
|
25
|
+
"version": "0.11.36",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.11.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.11.36",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.11.
|
|
57
|
+
"ref": "@blamejs/core@0.11.36",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|