diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2a13df..23b132a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
Cambios notables de la librería TypeScript y de la página. El proyecto usa versionado semántico; mientras sea 0.x, no hay promesa de estabilidad. La sección «Versiones» del [README](README.md) explica qué cubre cada número.
+## 0.6.0 — sin publicar
+
+- **El recordatorio en el calendario** (§62.1, regla 26, MAY), como `encrypt -reminder` de `datekeys-go` (`7e6a03d`): tras crear la cápsula, «Descargar el recordatorio» da `.recordatorio.ics`, un evento de iCalendar (RFC 5545) a la hora de la ronda, con una alarma y lo que hará falta para abrirla. `reminder.ts` escribe los mismos bytes que Go, y su prueba los compara con un texto calculado aparte, a partir del RFC: CRLF, líneas plegadas a 75 octetos sin cortar un carácter y el nombre escapado. El UID es un UUID al azar, que no nombra la cápsula; la página dice que un calendario que se sincroniza con un servidor sabrá el nombre y la fecha.
+
## 0.5.0 — 7 de octubre de 2026
La especificación 0.16, el tag `spec-v0.16` de `datekeys-go`: la revisión de Astra de la v0.15, con el sello sin `accuracy` y el JSON de drand estricto; y las palabras al azar de la llave de palabras, de una lista inglesa y una española, con el ordenador o con dados, y lo que dice el SDK oficial al sellar. El autor la cerró el 7 de octubre de 2026, con el tag `v0.5.0`.
diff --git a/README.md b/README.md
index ca32d51..c6aa994 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,8 @@ Hay tres números de versión, cada uno con su significado, como en la referenci
`version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos.
+En desarrollo está la `0.6.0` (`0.6.0-dev` en `package.json`): el recordatorio en el calendario de `/create` (§62.1, regla 26). Lo que cambia está en el [CHANGELOG](CHANGELOG.md).
+
La versión actual es la `0.5.0`, del 7 de octubre de 2026, con el tag `v0.5.0`. Cubre:
- la especificación 0.16 (el tag `spec-v0.16` de `datekeys-go`): lee los formatos de cápsula 1 a 3 y escribe el 3, y el 2 solo como generador de vectores (§62.1, regla 1);
- un sello sin `accuracy` no prueba nada antes de la fecha de apertura: los veredictos dicen por qué, y el escritor devuelve los del área que escribe (§29.7, §29.11, §62.1 regla 19);
@@ -232,6 +234,7 @@ Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una
| `src/lib/inspector/create-files.ts` | La lista de ficheros de `/create`, sin tablas: lo elegido y lo soltado, carpetas recorridas incluidas, los ficheros de un sistema fuera por defecto como en `collect.go`, las rutas editadas y lo que la cápsula guarda |
| `src/lib/inspector/create-check.ts` | Las reglas de las rutas y de los textos (§29.5, §29.6) como las explica `/create`: todos los problemas de todas las rutas, en español, con los de `pathrule.ts`. Se carga bajo demanda, con las tablas |
| `src/lib/inspector/create-input.ts` | El formulario de `/create`, sin DOM, reloj ni writer: `planCapsule` comprueba los campos en su orden y da lo que se muestra antes de cifrar, con los ficheros medidos una vez (`chooseFiles`); los destinatarios y los nombres de los ficheros |
+| `src/lib/inspector/reminder.ts` | El recordatorio que `/create` ofrece junto a la cápsula (§62.1, regla 26, MAY): un evento de iCalendar (RFC 5545) a la hora de la ronda, con una alarma y lo que hará falta para abrirla, byte a byte como `encrypt -reminder` de Go; el nombre del fichero, el de la cápsula con `.recordatorio.ics` |
| `src/lib/inspector/annex.ts` | El anexo de recuperación que `/create` ofrece junto a la cápsula: `annex/recovery.md`, que Vite publica con un nombre con hash, y el nombre de su fichero, el de la cápsula con `.recuperacion.txt`, como `RecoveryAnnexSuffix` de Go |
| `src/lib/inspector/create-words.ts` | La lista de las palabras al azar de `/create`: `wordListLoader` descarga una vez `wordlists/es.txt`, que Vite publica con un nombre con hash, y la lee con `readWordList`, con su SHA-256 fijado; un fallo no se guarda y la siguiente llamada lo vuelve a intentar |
| `src/lib/inspector/creator.ts` | La escritura, cargada bajo demanda: `encryptFiles` con los ficheros, el comentario y el autor hacia el fichero temporal o la memoria, con el progreso de las dos lecturas, la cancelación y la cuota, y los pasos 1 a 8 de lo escrito |
diff --git a/package-lock.json b/package-lock.json
index b9d9ec2..94958b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "datekeys-ts",
- "version": "0.5.0",
+ "version": "0.6.0-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "datekeys-ts",
- "version": "0.5.0",
+ "version": "0.6.0-dev",
"license": "Apache-2.0",
"dependencies": {
"@noble/ciphers": "2.4.0",
diff --git a/package.json b/package.json
index 3d9ba1c..e8f0a4b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "datekeys-ts",
- "version": "0.5.0",
+ "version": "0.6.0-dev",
"private": true,
"description": "DateKeys in TypeScript: canonical CBOR codec, DKC1/DKK1 parsers, capsule inspector library and its static inspector page; later, browser encryption and decryption.",
"license": "Apache-2.0",
diff --git a/src/lib/dkc/version.ts b/src/lib/dkc/version.ts
index dbac856..412e305 100644
--- a/src/lib/dkc/version.ts
+++ b/src/lib/dkc/version.ts
@@ -12,9 +12,10 @@
* 0.4.0 implements spec 0.15: the release object and a release in hand;
* 0.5.0 implements spec 0.16: a seal without accuracy proves nothing before
* the opening date and drand's JSON is read strictly; it also draws the
- * random words of a key of words, from a computer or from dice.
+ * random words of a key of words, from a computer or from dice; 0.6.0-dev is
+ * what comes after it.
*/
-export const VERSION = '0.5.0';
+export const VERSION = '0.6.0-dev';
/**
* The version of the DateKeys Protocol Specification that this library
diff --git a/src/lib/inspector/reminder.test.ts b/src/lib/inspector/reminder.test.ts
new file mode 100644
index 0000000..119d1fd
--- /dev/null
+++ b/src/lib/inspector/reminder.test.ts
@@ -0,0 +1,85 @@
+import { describe, expect, it } from 'vitest';
+import { REMINDER_SUFFIX, newUID, reminderICS, reminderName } from './reminder.ts';
+
+// The calendar file of the reminder of /create, byte for byte the one of
+// datekeys encrypt -reminder (cmd/datekeys/reminder_test.go of datekeys-go):
+// CRLF, lines folded at 75 octets without cutting a character, and a name
+// whose semicolon, comma and backslash are escaped (spec §62.1 rule 26, RFC
+// 5545). The expected text was computed apart from this code, from the RFC.
+describe('reminderICS', () => {
+ const base = {
+ name: 'carta; de, mamá\\.dkc',
+ round: 1000,
+ unlockMs: Date.UTC(2023, 7, 23, 15, 59, 24),
+ timeAndKey: true,
+ uid: '0f1e2d3c-4b5a-4697-8877-665544332211',
+ stampMs: Date.UTC(2026, 9, 7, 12, 0, 0),
+ };
+
+ it('writes the event of Go byte for byte', () => {
+ expect(reminderICS(base)).toBe(
+ 'BEGIN:VCALENDAR\r\n' +
+ 'VERSION:2.0\r\n' +
+ 'PRODID:-//DateKeys//datekeys-go//ES\r\n' +
+ 'CALSCALE:GREGORIAN\r\n' +
+ 'BEGIN:VEVENT\r\n' +
+ 'UID:0f1e2d3c-4b5a-4697-8877-665544332211\r\n' +
+ 'DTSTAMP:20261007T120000Z\r\n' +
+ 'DTSTART:20230823T155924Z\r\n' +
+ 'DTEND:20230823T162924Z\r\n' +
+ 'SUMMARY:Ya se puede abrir la cápsula DateKeys «carta\\; de\\, mamá\\\\.dkc»\r\n' +
+ 'DESCRIPTION:Desde este momento se puede abrir «carta\\; de\\, mamá\\\\.dkc».\r\n' +
+ ' Hace falta el fichero .dkc y una de sus llaves\\, y la firma de drand de l\r\n' +
+ ' a ronda 1000\\, que drand publica ahora: si un día ya no la sirve\\, un arc\r\n' +
+ ' hivo de firmas o un servicio de caché tiene que haberla guardado. Las ins\r\n' +
+ ' trucciones para abrirla sin DateKeys están en «carta\\; de\\, mamá\\\\.dkc.\r\n' +
+ ' recuperacion.txt».\r\n' +
+ 'TRANSP:TRANSPARENT\r\n' +
+ 'BEGIN:VALARM\r\n' +
+ 'ACTION:DISPLAY\r\n' +
+ 'TRIGGER:PT0S\r\n' +
+ 'DESCRIPTION:Ya se puede abrir la cápsula DateKeys «carta\\; de\\, mamá\\\\.d\r\n' +
+ ' kc»\r\n' +
+ 'END:VALARM\r\n' +
+ 'END:VEVENT\r\n' +
+ 'END:VCALENDAR\r\n',
+ );
+ });
+
+ it('folds every line at 75 octets', () => {
+ for (const line of reminderICS(base).split('\r\n')) {
+ expect(new TextEncoder().encode(line).length).toBeLessThanOrEqual(75);
+ }
+ });
+
+ it('asks for a credential only with a key', () => {
+ const ics = reminderICS({ ...base, timeAndKey: false, name: 'carta.dkc' });
+ // Unfolded, a line of RFC 5545 is its pieces without the CRLF and the space.
+ expect(ics.replaceAll('\r\n ', '')).toContain('Hace falta el fichero .dkc\\, y la firma');
+ expect(ics).not.toContain('una de sus llaves');
+ expect(ics).toContain('DTEND:20230823T162924Z\r\n');
+ });
+
+ it('escapes a line feed and drops a carriage return of a name', () => {
+ const ics = reminderICS({ ...base, name: 'a\r\nb.dkc' });
+ expect(ics).toContain('SUMMARY:Ya se puede abrir la cápsula DateKeys «a\\nb.dkc»\r\n');
+ });
+
+ it('folds before a character that the 75th octet would cut', () => {
+ // SUMMARY and the start of the text take 49 octets: with 25 more, ñ
+ // takes octets 75 and 76, and the line folds before it, at 74.
+ const ics = reminderICS({ ...base, name: `${'x'.repeat(25)}ñ.dkc` });
+ expect(ics).toContain(`SUMMARY:Ya se puede abrir la cápsula DateKeys «${'x'.repeat(25)}\r\n ñ.dkc»\r\n`);
+ });
+
+ it('names the file after the capsule', () => {
+ expect(reminderName('carta.dkc')).toBe('carta.dkc.recordatorio.ics');
+ expect(REMINDER_SUFFIX).toBe('.recordatorio.ics');
+ });
+
+ it('makes a random UUID of version 4', () => {
+ const a = newUID();
+ expect(a).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
+ expect(newUID()).not.toBe(a);
+ });
+});
diff --git a/src/lib/inspector/reminder.ts b/src/lib/inspector/reminder.ts
new file mode 100644
index 0000000..03b78d9
--- /dev/null
+++ b/src/lib/inspector/reminder.ts
@@ -0,0 +1,100 @@
+// The local reminder that /create offers next to a capsule, the MAY of spec
+// §62.1 rule 26: an iCalendar event (RFC 5545) at round_time, which any
+// calendar imports, with what opening the capsule will take. It is
+// cmd/datekeys/reminder.go of datekeys-go, byte for byte. It holds the name
+// of the capsule and its date, and nothing secret; a calendar that syncs
+// with a server learns both.
+
+/** What is appended to the name of the .dkc to name its reminder: carta.dkc.recordatorio.ics. */
+export const REMINDER_SUFFIX = '.recordatorio.ics';
+
+/** The name of the reminder of the capsule saved as `dkcName`. */
+export function reminderName(dkcName: string): string {
+ return `${dkcName}${REMINDER_SUFFIX}`;
+}
+
+/** A reminder: the capsule, its round and round_time, and the event. */
+export interface Reminder {
+ /** The file name of the capsule. */
+ readonly name: string;
+ readonly round: number;
+ /** round_time, in milliseconds since the epoch. */
+ readonly unlockMs: number;
+ /** Whether opening needs one of its credentials too. */
+ readonly timeAndKey: boolean;
+ /** A random UUID, so that the event names no capsule. */
+ readonly uid: string;
+ /** When the event was made, in milliseconds since the epoch. */
+ readonly stampMs: number;
+}
+
+/** A random UUID of version 4 (RFC 9562). */
+export function newUID(): string {
+ const b = crypto.getRandomValues(new Uint8Array(16));
+ b[6] = (b[6]! & 0x0f) | 0x40;
+ b[8] = (b[8]! & 0x3f) | 0x80;
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
+}
+
+// A TEXT value of RFC 5545 (3.3.11): a backslash, a semicolon, a comma and
+// a line feed escaped; a carriage return goes.
+function calendarText(s: string): string {
+ return s.replace(/[\\;,\n\r]/g, (c) => (c === '\n' ? '\\n' : c === '\r' ? '' : `\\${c}`));
+}
+
+// A DATE-TIME of RFC 5545 in UTC, with a Z: 20230823T155924Z.
+function calendarTime(ms: number): string {
+ return new Date(ms).toISOString().replace(/\.\d+Z$/, 'Z').replace(/[-:]/g, '');
+}
+
+// A content line ended with CRLF and folded at 75 octets (RFC 5545, 3.1):
+// each continuation starts with a space, and no UTF-8 sequence is cut.
+function foldLine(line: string): string {
+ let rest = new TextEncoder().encode(line);
+ const parts: Uint8Array[] = [];
+ let limit = 75;
+ while (rest.length > limit) {
+ let cut = limit;
+ while (cut > 0 && (rest[cut]! & 0xc0) === 0x80) cut--;
+ parts.push(rest.subarray(0, cut));
+ rest = rest.subarray(cut);
+ limit = 74;
+ }
+ parts.push(rest);
+ const decoder = new TextDecoder();
+ return `${parts.map((p) => decoder.decode(p)).join('\r\n ')}\r\n`;
+}
+
+/** The calendar file of the reminder, in Spanish, as the annex is. */
+export function reminderICS(r: Reminder): string {
+ const summary = `Ya se puede abrir la cápsula DateKeys «${r.name}»`;
+ const needs = r.timeAndKey ? 'el fichero .dkc y una de sus llaves' : 'el fichero .dkc';
+ const description =
+ `Desde este momento se puede abrir «${r.name}». Hace falta ${needs}, y la firma de drand de la ronda ${r.round}, ` +
+ 'que drand publica ahora: si un día ya no la sirve, un archivo de firmas o un servicio de caché tiene que haberla guardado. ' +
+ `Las instrucciones para abrirla sin DateKeys están en «${r.name}.recuperacion.txt».`;
+ return [
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//DateKeys//datekeys-go//ES',
+ 'CALSCALE:GREGORIAN',
+ 'BEGIN:VEVENT',
+ `UID:${r.uid}`,
+ `DTSTAMP:${calendarTime(r.stampMs)}`,
+ `DTSTART:${calendarTime(r.unlockMs)}`,
+ `DTEND:${calendarTime(r.unlockMs + 30 * 60 * 1000)}`,
+ `SUMMARY:${calendarText(summary)}`,
+ `DESCRIPTION:${calendarText(description)}`,
+ 'TRANSP:TRANSPARENT',
+ 'BEGIN:VALARM',
+ 'ACTION:DISPLAY',
+ 'TRIGGER:PT0S',
+ `DESCRIPTION:${calendarText(summary)}`,
+ 'END:VALARM',
+ 'END:VEVENT',
+ 'END:VCALENDAR',
+ ]
+ .map(foldLine)
+ .join('');
+}
diff --git a/src/routes/create/+page.svelte b/src/routes/create/+page.svelte
index e01160c..09c031d 100644
--- a/src/routes/create/+page.svelte
+++ b/src/routes/create/+page.svelte
@@ -44,6 +44,7 @@
} from '$lib/inspector/create-input.ts';
import { wordListLoader, WORDS_LANGUAGE } from '$lib/inspector/create-words.ts';
import { ANNEX_URL, annexName } from '$lib/inspector/annex.ts';
+ import { newUID, reminderICS, reminderName } from '$lib/inspector/reminder.ts';
import { escapeInvisible, formatByteCount, formatDateTime, formatInteger, formatRelative, unexpectedProblem, viewerTimeZone } from '$lib/inspector/format.ts';
import { isTimeZone, localParts, supportedTimeZones, timeZoneList, UTC } from '$lib/inspector/localtime.ts';
import { buildReport, type Report } from '$lib/inspector/report.ts';
@@ -438,6 +439,23 @@
return url;
}
+ // The local reminder of spec §62.1 rule 26 (MAY): a calendar event at the
+ // round time, with what opening will take. Its UID is random, so that it
+ // names no capsule.
+ function saveReminder(): void {
+ if (result === undefined) return;
+ const name = downloadName(dkcName, '.dkc', result.plan.names.dkc);
+ const ics = reminderICS({
+ name,
+ round: result.plan.dateKey.round,
+ unlockMs: result.plan.effectiveMs,
+ timeAndKey: result.plan.policy === TIME_AND_KEY,
+ uid: newUID(),
+ stampMs: Date.now(),
+ });
+ save(new Blob([ics], { type: 'text/calendar' }), reminderName(name));
+ }
+
function saveCapsule(): void {
if (result === undefined || capsuleGone) return;
save(result.capsule, downloadName(dkcName, '.dkc', result.plan.names.dkc));
@@ -1417,10 +1435,12 @@
Guarda con la cápsula las instrucciones para abrirla sin DateKeys, por si ya no existe. Son las mismas para toda cápsula y
- no dicen nada de la tuya.
+ no dicen nada de la tuya. El recordatorio es un evento para tu calendario a esa hora, con el nombre de la cápsula: si el
+ calendario se sincroniza con un servidor, este sabrá el nombre y la fecha.
diff --git a/vitest.config.ts b/vitest.config.ts
index 404e7eb..849fecb 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -51,6 +51,7 @@ export default defineConfig({
'src/lib/inspector/create-input.ts': { 100: true },
'src/lib/inspector/create-words.ts': { 100: true },
'src/lib/inspector/annex.ts': { 100: true },
+ 'src/lib/inspector/reminder.ts': { 100: true },
'src/lib/inspector/creator.ts': { 100: true },
// Format 3 (plan of format 3 in datekeys-ts): the rules of the paths
// and texts of the head, the codec of BODY, of the security area and of