@lanes-sh/link 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iCalendar (RFC 5545) and vCard (RFC 6350), to the depth this needs.
|
|
3
|
+
*
|
|
4
|
+
* Small on purpose. The genuinely hard part of RFC 5545 is recurrence — RRULE
|
|
5
|
+
* with EXDATE, RECURRENCE-ID and VTIMEZONE — and **CalDAV lets the server do
|
|
6
|
+
* it**: a `calendar-query` carrying `<C:expand>` returns instances already
|
|
7
|
+
* expanded. What is left is mechanical and precisely specified: unfold
|
|
8
|
+
* continuation lines, split `NAME;PARAM=value:VALUE`, unescape, and understand
|
|
9
|
+
* three forms of date.
|
|
10
|
+
*
|
|
11
|
+
* That is why there is no `ical.js` here. It is 200 KB whose centrepiece is the
|
|
12
|
+
* recurrence engine the protocol just did for us, under MPL-2.0 — a licence
|
|
13
|
+
* nothing else in this tree carries.
|
|
14
|
+
*
|
|
15
|
+
* Where a server ignores `<C:expand>`, the caller reports `expanded: false` and
|
|
16
|
+
* hands back the RRULE verbatim. Degrading honestly beats expanding wrongly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface ContentLine {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly params: Readonly<Record<string, string>>;
|
|
22
|
+
readonly value: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Unfold and split a whole document into lines.
|
|
27
|
+
*
|
|
28
|
+
* Unfolding first, and it must be first: a line beginning with a space or tab is
|
|
29
|
+
* a continuation of the previous one, and a long `SUMMARY` is routinely folded
|
|
30
|
+
* mid-word at 75 octets.
|
|
31
|
+
*/
|
|
32
|
+
export function parseLines(text: string): ContentLine[] {
|
|
33
|
+
const unfolded = text.replace(/\r?\n[ \t]/g, '');
|
|
34
|
+
|
|
35
|
+
return unfolded
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.filter((line) => line.trim() !== '')
|
|
38
|
+
.map(parseLine)
|
|
39
|
+
.filter((line): line is ContentLine => line !== null);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseLine(line: string): ContentLine | null {
|
|
43
|
+
// The value starts after the first colon that is not inside a quoted
|
|
44
|
+
// parameter — `ATTENDEE;CN="Smith, J:r":mailto:j@x` is legal and rare enough
|
|
45
|
+
// that a naive split on the first colon truncates it silently.
|
|
46
|
+
let inQuotes = false;
|
|
47
|
+
let colon = -1;
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index < line.length; index++) {
|
|
50
|
+
const character = line[index];
|
|
51
|
+
if (character === '"') inQuotes = !inQuotes;
|
|
52
|
+
else if (character === ':' && !inQuotes) {
|
|
53
|
+
colon = index;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (colon === -1) return null;
|
|
59
|
+
|
|
60
|
+
const head = line.slice(0, colon);
|
|
61
|
+
const value = unescapeValue(line.slice(colon + 1));
|
|
62
|
+
|
|
63
|
+
const [name, ...rest] = head.split(';');
|
|
64
|
+
const params: Record<string, string> = {};
|
|
65
|
+
|
|
66
|
+
for (const parameter of rest) {
|
|
67
|
+
const equals = parameter.indexOf('=');
|
|
68
|
+
if (equals === -1) continue;
|
|
69
|
+
params[parameter.slice(0, equals).toUpperCase()] = parameter
|
|
70
|
+
.slice(equals + 1)
|
|
71
|
+
.replace(/^"|"$/g, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { name: (name ?? '').toUpperCase(), params, value };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function unescapeValue(value: string): string {
|
|
78
|
+
return value
|
|
79
|
+
.replace(/\\n/gi, '\n')
|
|
80
|
+
.replace(/\\,/g, ',')
|
|
81
|
+
.replace(/\\;/g, ';')
|
|
82
|
+
.replace(/\\\\/g, '\\');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function escapeValue(value: string): string {
|
|
86
|
+
return value
|
|
87
|
+
.replace(/\\/g, '\\\\')
|
|
88
|
+
.replace(/;/g, '\\;')
|
|
89
|
+
.replace(/,/g, '\\,')
|
|
90
|
+
.replace(/\n/g, '\\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CalendarEvent {
|
|
94
|
+
readonly uid: string | null;
|
|
95
|
+
readonly summary: string | null;
|
|
96
|
+
readonly start: string | null;
|
|
97
|
+
readonly end: string | null;
|
|
98
|
+
readonly all_day: boolean;
|
|
99
|
+
readonly location: string | null;
|
|
100
|
+
readonly description: string | null;
|
|
101
|
+
readonly organizer: string | null;
|
|
102
|
+
readonly attendees: readonly string[];
|
|
103
|
+
readonly status: string | null;
|
|
104
|
+
/** Present only when the server declined to expand a repeating event. */
|
|
105
|
+
readonly recurrence_rule?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The VEVENTs in one iCalendar document. */
|
|
109
|
+
export function parseEvents(text: string): CalendarEvent[] {
|
|
110
|
+
const events: CalendarEvent[] = [];
|
|
111
|
+
let current: ContentLine[] | null = null;
|
|
112
|
+
|
|
113
|
+
for (const line of parseLines(text)) {
|
|
114
|
+
if (line.name === 'BEGIN' && line.value === 'VEVENT') {
|
|
115
|
+
current = [];
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (line.name === 'END' && line.value === 'VEVENT') {
|
|
119
|
+
if (current) events.push(toEvent(current));
|
|
120
|
+
current = null;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
current?.push(line);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return events;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toEvent(lines: readonly ContentLine[]): CalendarEvent {
|
|
130
|
+
const first = (name: string): ContentLine | undefined => lines.find((l) => l.name === name);
|
|
131
|
+
const start = first('DTSTART');
|
|
132
|
+
const rrule = first('RRULE');
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
uid: first('UID')?.value ?? null,
|
|
136
|
+
summary: first('SUMMARY')?.value ?? null,
|
|
137
|
+
start: start ? normaliseDate(start) : null,
|
|
138
|
+
end: (() => {
|
|
139
|
+
const end = first('DTEND');
|
|
140
|
+
return end ? normaliseDate(end) : null;
|
|
141
|
+
})(),
|
|
142
|
+
// `VALUE=DATE` is the marker for an all-day event; the absence of a time is
|
|
143
|
+
// not incidental formatting.
|
|
144
|
+
all_day: start?.params['VALUE'] === 'DATE',
|
|
145
|
+
location: first('LOCATION')?.value ?? null,
|
|
146
|
+
description: first('DESCRIPTION')?.value ?? null,
|
|
147
|
+
organizer: first('ORGANIZER')?.value.replace(/^mailto:/i, '') ?? null,
|
|
148
|
+
attendees: lines
|
|
149
|
+
.filter((line) => line.name === 'ATTENDEE')
|
|
150
|
+
.map((line) => line.value.replace(/^mailto:/i, '')),
|
|
151
|
+
status: first('STATUS')?.value ?? null,
|
|
152
|
+
...(rrule ? { recurrence_rule: rrule.value } : {}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The three forms a DTSTART takes, as ISO 8601.
|
|
158
|
+
*
|
|
159
|
+
* `VALUE=DATE:20260810`, `TZID=Europe/London:20260810T090000`, and
|
|
160
|
+
* `20260810T080000Z`. The middle one keeps its zone as a suffix rather than
|
|
161
|
+
* being converted: converting needs a tz database, and guessing is worse than
|
|
162
|
+
* saying which zone the server meant.
|
|
163
|
+
*/
|
|
164
|
+
export function normaliseDate(line: ContentLine): string {
|
|
165
|
+
const value = line.value.trim();
|
|
166
|
+
const match = value.match(/^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z)?)?$/);
|
|
167
|
+
if (!match) return value;
|
|
168
|
+
|
|
169
|
+
const [, year, month, day, hour, minute, second, zulu] = match;
|
|
170
|
+
if (!hour) return `${year}-${month}-${day}`;
|
|
171
|
+
|
|
172
|
+
const stamp = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
|
|
173
|
+
if (zulu) return `${stamp}Z`;
|
|
174
|
+
|
|
175
|
+
const zone = line.params['TZID'];
|
|
176
|
+
return zone ? `${stamp} (${zone})` : stamp;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** ISO 8601 in, iCalendar's compact form out. */
|
|
180
|
+
function toIcalDate(value: string, allDay: boolean): string {
|
|
181
|
+
const digits = value.replace(/[-:]/g, '');
|
|
182
|
+
if (allDay) return digits.slice(0, 8);
|
|
183
|
+
if (digits.endsWith('Z')) return digits.replace(/\.\d+Z$/, 'Z').slice(0, 16) + 'Z';
|
|
184
|
+
return `${digits.slice(0, 15)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface NewEvent {
|
|
188
|
+
readonly uid: string;
|
|
189
|
+
readonly summary: string;
|
|
190
|
+
readonly start: string;
|
|
191
|
+
readonly end: string;
|
|
192
|
+
readonly allDay?: boolean | undefined;
|
|
193
|
+
readonly location?: string | undefined;
|
|
194
|
+
readonly description?: string | undefined;
|
|
195
|
+
readonly timestamp: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Build a minimal, valid VCALENDAR carrying one VEVENT. */
|
|
199
|
+
export function buildEvent(event: NewEvent): string {
|
|
200
|
+
const allDay = event.allDay === true;
|
|
201
|
+
const dateParam = allDay ? ';VALUE=DATE' : '';
|
|
202
|
+
|
|
203
|
+
const lines = [
|
|
204
|
+
'BEGIN:VCALENDAR',
|
|
205
|
+
'VERSION:2.0',
|
|
206
|
+
'PRODID:-//Lanes Link//EN',
|
|
207
|
+
'BEGIN:VEVENT',
|
|
208
|
+
`UID:${event.uid}`,
|
|
209
|
+
`DTSTAMP:${toIcalDate(event.timestamp, false)}`,
|
|
210
|
+
`DTSTART${dateParam}:${toIcalDate(event.start, allDay)}`,
|
|
211
|
+
`DTEND${dateParam}:${toIcalDate(event.end, allDay)}`,
|
|
212
|
+
`SUMMARY:${escapeValue(event.summary)}`,
|
|
213
|
+
...(event.location ? [`LOCATION:${escapeValue(event.location)}`] : []),
|
|
214
|
+
...(event.description ? [`DESCRIPTION:${escapeValue(event.description)}`] : []),
|
|
215
|
+
'END:VEVENT',
|
|
216
|
+
'END:VCALENDAR',
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
// CRLF, which RFC 5545 requires and some servers enforce.
|
|
220
|
+
return `${lines.map(fold).join('\r\n')}\r\n`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Fold at 75 octets, as the spec requires for long values. */
|
|
224
|
+
function fold(line: string): string {
|
|
225
|
+
if (line.length <= 75) return line;
|
|
226
|
+
const parts = [line.slice(0, 75)];
|
|
227
|
+
let rest = line.slice(75);
|
|
228
|
+
while (rest.length > 74) {
|
|
229
|
+
parts.push(` ${rest.slice(0, 74)}`);
|
|
230
|
+
rest = rest.slice(74);
|
|
231
|
+
}
|
|
232
|
+
parts.push(` ${rest}`);
|
|
233
|
+
return parts.join('\r\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface EventPatch {
|
|
237
|
+
readonly summary?: string | undefined;
|
|
238
|
+
readonly start?: string | undefined;
|
|
239
|
+
readonly end?: string | undefined;
|
|
240
|
+
readonly allDay?: boolean | undefined;
|
|
241
|
+
readonly location?: string | undefined;
|
|
242
|
+
readonly description?: string | undefined;
|
|
243
|
+
readonly timestamp: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Edit an existing VEVENT in place, by patching its text.
|
|
248
|
+
*
|
|
249
|
+
* Rebuilding the event from the fields we model would silently discard
|
|
250
|
+
* everything we do not — attendees and their reply status, alarms, the RRULE, a
|
|
251
|
+
* conferencing URL, `X-` properties another client depends on. Changing the time
|
|
252
|
+
* of a meeting must not un-invite everyone, so the untouched lines are carried
|
|
253
|
+
* through verbatim and only the named properties are replaced.
|
|
254
|
+
*
|
|
255
|
+
* `SEQUENCE` is bumped and `DTSTAMP` refreshed because that is how every other
|
|
256
|
+
* calendar client learns something changed; without it the edit lands on the
|
|
257
|
+
* server and no device notices.
|
|
258
|
+
*/
|
|
259
|
+
export function patchEvent(ical: string, patch: EventPatch): string {
|
|
260
|
+
const lines = ical.replace(/\r?\n[ \t]/g, '').split(/\r?\n/);
|
|
261
|
+
|
|
262
|
+
const start = lines.findIndex((line) => line.trim() === 'BEGIN:VEVENT');
|
|
263
|
+
const end = lines.findIndex((line) => line.trim() === 'END:VEVENT');
|
|
264
|
+
if (start === -1 || end === -1) throw new Error('That entry does not contain an event to edit.');
|
|
265
|
+
|
|
266
|
+
const nameOf = (line: string): string => (line.split(/[;:]/)[0] ?? '').toUpperCase();
|
|
267
|
+
const body = lines.slice(start + 1, end);
|
|
268
|
+
|
|
269
|
+
const allDay =
|
|
270
|
+
patch.allDay ?? body.some((line) => nameOf(line) === 'DTSTART' && /VALUE=DATE[:;]/.test(line));
|
|
271
|
+
const dateParam = allDay ? ';VALUE=DATE' : '';
|
|
272
|
+
|
|
273
|
+
const replacements = new Map<string, string | null>();
|
|
274
|
+
if (patch.summary !== undefined) replacements.set('SUMMARY', `SUMMARY:${escapeValue(patch.summary)}`);
|
|
275
|
+
if (patch.location !== undefined) {
|
|
276
|
+
replacements.set('LOCATION', patch.location ? `LOCATION:${escapeValue(patch.location)}` : null);
|
|
277
|
+
}
|
|
278
|
+
if (patch.description !== undefined) {
|
|
279
|
+
replacements.set(
|
|
280
|
+
'DESCRIPTION',
|
|
281
|
+
patch.description ? `DESCRIPTION:${escapeValue(patch.description)}` : null,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
// Replacing the whole line drops a stale `TZID` along with it, which matters:
|
|
285
|
+
// keeping the old zone against a new UTC time silently moves the meeting.
|
|
286
|
+
if (patch.start !== undefined) {
|
|
287
|
+
replacements.set('DTSTART', `DTSTART${dateParam}:${toIcalDate(patch.start, allDay)}`);
|
|
288
|
+
}
|
|
289
|
+
if (patch.end !== undefined) {
|
|
290
|
+
replacements.set('DTEND', `DTEND${dateParam}:${toIcalDate(patch.end, allDay)}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
replacements.set('DTSTAMP', `DTSTAMP:${toIcalDate(patch.timestamp, false)}`);
|
|
294
|
+
|
|
295
|
+
const current = body.find((line) => nameOf(line) === 'SEQUENCE');
|
|
296
|
+
const sequence = current ? Number(current.split(':')[1] ?? 0) || 0 : 0;
|
|
297
|
+
replacements.set('SEQUENCE', `SEQUENCE:${sequence + 1}`);
|
|
298
|
+
|
|
299
|
+
const seen = new Set<string>();
|
|
300
|
+
const patched: string[] = [];
|
|
301
|
+
|
|
302
|
+
for (const line of body) {
|
|
303
|
+
const name = nameOf(line);
|
|
304
|
+
if (!replacements.has(name)) {
|
|
305
|
+
patched.push(line);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
seen.add(name);
|
|
309
|
+
const replacement = replacements.get(name);
|
|
310
|
+
if (replacement !== null && replacement !== undefined) patched.push(replacement);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A property being set for the first time has nothing to replace.
|
|
314
|
+
for (const [name, replacement] of replacements) {
|
|
315
|
+
if (!seen.has(name) && replacement) patched.push(replacement);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const rebuilt = [...lines.slice(0, start + 1), ...patched, ...lines.slice(end)];
|
|
319
|
+
return `${rebuilt.filter((line) => line !== '').map(fold).join('\r\n')}\r\n`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface NewContact {
|
|
323
|
+
readonly uid: string;
|
|
324
|
+
readonly fullName: string;
|
|
325
|
+
readonly emails?: readonly string[] | undefined;
|
|
326
|
+
readonly phones?: readonly string[] | undefined;
|
|
327
|
+
readonly organization?: string | undefined;
|
|
328
|
+
readonly note?: string | undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Build a vCard 3.0.
|
|
333
|
+
*
|
|
334
|
+
* 3.0 rather than 4.0 because it is what Apple writes and what every client
|
|
335
|
+
* reads; 4.0 support is uneven enough that the safe choice is the older one.
|
|
336
|
+
*/
|
|
337
|
+
export function buildContact(contact: NewContact): string {
|
|
338
|
+
// `N` is structured as family;given;middle;prefix;suffix. Splitting a display
|
|
339
|
+
// name on the last space is a guess, but an absent `N` breaks sorting in
|
|
340
|
+
// Contacts, and a wrong guess is visible and fixable where a missing field is
|
|
341
|
+
// neither.
|
|
342
|
+
const parts = contact.fullName.trim().split(/\s+/);
|
|
343
|
+
const family = parts.length > 1 ? parts[parts.length - 1]! : '';
|
|
344
|
+
const given = parts.length > 1 ? parts.slice(0, -1).join(' ') : contact.fullName;
|
|
345
|
+
|
|
346
|
+
const lines = [
|
|
347
|
+
'BEGIN:VCARD',
|
|
348
|
+
'VERSION:3.0',
|
|
349
|
+
`UID:${contact.uid}`,
|
|
350
|
+
`FN:${escapeValue(contact.fullName)}`,
|
|
351
|
+
`N:${escapeValue(family)};${escapeValue(given)};;;`,
|
|
352
|
+
...(contact.emails ?? []).map((email) => `EMAIL;TYPE=INTERNET:${escapeValue(email)}`),
|
|
353
|
+
...(contact.phones ?? []).map((phone) => `TEL;TYPE=CELL:${escapeValue(phone)}`),
|
|
354
|
+
...(contact.organization ? [`ORG:${escapeValue(contact.organization)}`] : []),
|
|
355
|
+
...(contact.note ? [`NOTE:${escapeValue(contact.note)}`] : []),
|
|
356
|
+
'END:VCARD',
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
return `${lines.map(fold).join('\r\n')}\r\n`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface Contact {
|
|
363
|
+
readonly uid: string | null;
|
|
364
|
+
readonly full_name: string | null;
|
|
365
|
+
readonly emails: readonly string[];
|
|
366
|
+
readonly phones: readonly string[];
|
|
367
|
+
readonly organization: string | null;
|
|
368
|
+
readonly note: string | null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The VCARDs in one document. vCard 3.0 and 4.0 share this grammar. */
|
|
372
|
+
export function parseContacts(text: string): Contact[] {
|
|
373
|
+
const contacts: Contact[] = [];
|
|
374
|
+
let current: ContentLine[] | null = null;
|
|
375
|
+
|
|
376
|
+
for (const line of parseLines(text)) {
|
|
377
|
+
if (line.name === 'BEGIN' && line.value === 'VCARD') {
|
|
378
|
+
current = [];
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (line.name === 'END' && line.value === 'VCARD') {
|
|
382
|
+
if (current) contacts.push(toContact(current));
|
|
383
|
+
current = null;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
current?.push(line);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return contacts;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function toContact(lines: readonly ContentLine[]): Contact {
|
|
393
|
+
const first = (name: string): string | null =>
|
|
394
|
+
lines.find((line) => line.name === name)?.value ?? null;
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
uid: first('UID'),
|
|
398
|
+
// FN is the display name; N is the structured one and is not always present.
|
|
399
|
+
full_name: first('FN') ?? first('N')?.split(';').filter(Boolean).reverse().join(' ') ?? null,
|
|
400
|
+
emails: lines.filter((line) => line.name === 'EMAIL').map((line) => line.value),
|
|
401
|
+
phones: lines.filter((line) => line.name === 'TEL').map((line) => line.value),
|
|
402
|
+
organization: first('ORG')?.split(';')[0] ?? null,
|
|
403
|
+
note: first('NOTE'),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export /** iCalendar's compact UTC stamp, which time-range filters require. */
|
|
408
|
+
function compact(value: string): string {
|
|
409
|
+
const date = new Date(value);
|
|
410
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
411
|
+
return `${date.toISOString().replace(/[-:]/g, '').split('.')[0]}Z`;
|
|
412
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Connector, ToolResult } from '#connectivity';
|
|
2
|
+
import { davCapabilities } from './capabilities.ts';
|
|
3
|
+
import { createContact, searchContacts } from './contacts.ts';
|
|
4
|
+
import {
|
|
5
|
+
createEvent,
|
|
6
|
+
deleteEvent,
|
|
7
|
+
getEvent,
|
|
8
|
+
listEvents,
|
|
9
|
+
updateEvent,
|
|
10
|
+
} from './calendar.ts';
|
|
11
|
+
import { DavClient } from './client.ts';
|
|
12
|
+
import { OPERATIONS } from './operations.ts';
|
|
13
|
+
import { error, validate } from './request.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `dav` connector — CalDAV and CardDAV.
|
|
17
|
+
*
|
|
18
|
+
* One kind for both: they share the discovery dance and the `REPORT` machinery
|
|
19
|
+
* and differ only in a namespace and a payload format. Vendor-neutral — iCloud,
|
|
20
|
+
* Fastmail, Nextcloud and a Radicale on a Raspberry Pi are the same two manifest
|
|
21
|
+
* fields.
|
|
22
|
+
*
|
|
23
|
+
* It is HTTPS, so `context.authorize` attaches Basic auth and this file never
|
|
24
|
+
* sees a credential. That is the payoff for keeping auth orthogonal to
|
|
25
|
+
* connectivity: a whole new protocol cost nothing on the credential side.
|
|
26
|
+
*
|
|
27
|
+
* **Where the account-specific routing lives.** Discovery resolves a *per
|
|
28
|
+
* account* home URL — iCloud answers on a numbered partition host like
|
|
29
|
+
* `p42-caldav.icloud.com` — and that must not go into
|
|
30
|
+
* `DiscoveredCapability.target`, because the discovery cache is keyed by
|
|
31
|
+
* provider rather than by connection. Two Apple accounts would share one entry,
|
|
32
|
+
* and the second would be pointed at the first one's calendars. It goes in
|
|
33
|
+
* per-connection state instead, which is already namespaced
|
|
34
|
+
* `<provider>/<connection>` and lives in the state store, so a cold instance still serves
|
|
35
|
+
* without re-discovering.
|
|
36
|
+
*
|
|
37
|
+
* The file itself is the wiring and nothing more: what is exposed is
|
|
38
|
+
* `capabilities.ts`, how a request is made is `request.ts`, the session is
|
|
39
|
+
* `client.ts`, and the operations are `calendar.ts` and `contacts.ts`.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface DavConnectorOptions {
|
|
43
|
+
readonly baseUrl: string;
|
|
44
|
+
readonly service: 'caldav' | 'carddav';
|
|
45
|
+
/**
|
|
46
|
+
* Longest `list_events` window this server will answer, declared by the
|
|
47
|
+
* provider. A server limit is the vendor's fact, not this transport's.
|
|
48
|
+
*/
|
|
49
|
+
readonly maxRangeDays?: number | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* One line to append when the server refuses the credential, in the
|
|
52
|
+
* provider's own words. A transport can say "refused"; only the provider
|
|
53
|
+
* knows why that usually happens for its vendor.
|
|
54
|
+
*/
|
|
55
|
+
readonly troubleshooting?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Used only to prove the credential works at `connect` time and to report
|
|
58
|
+
* whose account it is. Every *operation* goes through `context.authorize`, so
|
|
59
|
+
* this connector still has no idea where credentials are kept.
|
|
60
|
+
*/
|
|
61
|
+
readonly credential?: (() => Promise<{ username: string; password: string }>) | undefined;
|
|
62
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Collection {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly href: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createDavConnector(options: DavConnectorOptions): Connector {
|
|
71
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
kind: 'dav',
|
|
75
|
+
|
|
76
|
+
async discover() {
|
|
77
|
+
const capabilities = davCapabilities(options.service);
|
|
78
|
+
|
|
79
|
+
// Prove the credential before declaring the connection good. Without this
|
|
80
|
+
// a wrong app-specific password surfaces as a failed tool call days later
|
|
81
|
+
// rather than as a failed `connect` now.
|
|
82
|
+
await validate(options, doFetch);
|
|
83
|
+
|
|
84
|
+
return capabilities;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whose account this is: the name the *server accepted*.
|
|
89
|
+
*
|
|
90
|
+
* Same answer as `imap` gives, for the same reason — there is no endpoint
|
|
91
|
+
* returning an email address, and the username is the identity. Returning it
|
|
92
|
+
* only after a successful PROPFIND is what makes it a claim rather than an
|
|
93
|
+
* echo of what was typed.
|
|
94
|
+
*/
|
|
95
|
+
async identify(): Promise<string | null> {
|
|
96
|
+
return validate(options, doFetch);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async invoke(capability, args, context): Promise<ToolResult> {
|
|
100
|
+
const operation = String(capability.target?.['operation'] ?? capability.name);
|
|
101
|
+
const dav = new DavClient(options, doFetch, context);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
switch (operation) {
|
|
105
|
+
case OPERATIONS.listCalendars:
|
|
106
|
+
case OPERATIONS.listAddressbooks:
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: JSON.stringify(
|
|
112
|
+
options.service === 'caldav'
|
|
113
|
+
? { calendars: await dav.collections() }
|
|
114
|
+
: { addressbooks: await dav.collections() },
|
|
115
|
+
null,
|
|
116
|
+
2,
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
case OPERATIONS.listEvents:
|
|
122
|
+
return await listEvents(dav, args);
|
|
123
|
+
case OPERATIONS.getEvent:
|
|
124
|
+
return await getEvent(dav, args);
|
|
125
|
+
case OPERATIONS.createEvent:
|
|
126
|
+
return await createEvent(dav, args);
|
|
127
|
+
case OPERATIONS.updateEvent:
|
|
128
|
+
return await updateEvent(dav, args);
|
|
129
|
+
case OPERATIONS.deleteEvent:
|
|
130
|
+
return await deleteEvent(dav, args);
|
|
131
|
+
case OPERATIONS.searchContacts:
|
|
132
|
+
return await searchContacts(dav, args);
|
|
133
|
+
case OPERATIONS.createContact:
|
|
134
|
+
return await createContact(dav, args);
|
|
135
|
+
default:
|
|
136
|
+
return error(`Unknown operation "${operation}".`);
|
|
137
|
+
}
|
|
138
|
+
} catch (failure) {
|
|
139
|
+
return error((failure as Error).message);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operation names a DAV connection exposes, and the two constants that
|
|
3
|
+
* shape them.
|
|
4
|
+
*
|
|
5
|
+
* Their own file because `capabilities.ts` declares them and `index.ts`
|
|
6
|
+
* dispatches on them: a name spelled differently in the two places is a tool
|
|
7
|
+
* that lists and cannot be called, which is exactly the failure a shared
|
|
8
|
+
* constant prevents.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const OPERATIONS = {
|
|
12
|
+
listCalendars: 'list_calendars',
|
|
13
|
+
listEvents: 'list_events',
|
|
14
|
+
getEvent: 'get_event',
|
|
15
|
+
createEvent: 'create_event',
|
|
16
|
+
updateEvent: 'update_event',
|
|
17
|
+
deleteEvent: 'delete_event',
|
|
18
|
+
listAddressbooks: 'list_addressbooks',
|
|
19
|
+
searchContacts: 'search_contacts',
|
|
20
|
+
createContact: 'create_contact',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** Where the resolved, account-specific collection home is remembered. */
|
|
24
|
+
export const HOME_KEY = 'dav:home';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default `list_events` window, when a provider declares none.
|
|
28
|
+
*
|
|
29
|
+
* Every DAV server slows down over a year of expanded recurrences, so there is
|
|
30
|
+
* a bound rather than none. Which bound is a *server* fact, so a provider that
|
|
31
|
+
* knows its own says so — see `max_range_days` on the connector schema.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_MAX_RANGE_DAYS = 366;
|
|
34
|
+
|
|
35
|
+
export const object = (
|
|
36
|
+
properties: Record<string, unknown>,
|
|
37
|
+
required: string[] = [],
|
|
38
|
+
): Record<string, unknown> => ({
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties,
|
|
41
|
+
...(required.length > 0 ? { required } : {}),
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
});
|