From 5d5dd031b9458b2d4a3379aaade60aa41d21b311 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Thu, 22 Jan 2026 09:26:19 +0100
Subject: [PATCH] API documentation changes for groupware-apidocs
* add example generator infrastructure, with some examples for pkg/jmap
and pkg/groupware, with more needing to be done
* alter the apidoc Makefile to stop using go-swagger but, instead, use
the openapi.yml file that must be dropped into that directory using
groupware-apidocs (will improve the integration there later)
* add Makefile target to generate examples
* bump redocly from 2.4.0 to 2.14.5
* introduce Request.PathParam() and .PathParamDoc() to improve API
documentation, as well as future-proofing
* improve X-Request-ID and Trace-Id header handling in the middleware
by logging it safely when an error occurs in the middleware
---
pkg/jmap/.gitignore | 1 +
pkg/jmap/jmap_api_quota.go | 2 +-
pkg/jmap/jmap_model.go | 18 +-
pkg/jmap/jmap_model_examples.go | 694 ++++++++++++++++++
pkg/jmap/jmap_model_examples_test.go | 8 +
pkg/jmap/jmap_tools.go | 8 +
pkg/jscalendar/.gitignore | 1 +
pkg/jscontact/.gitignore | 1 +
services/groupware/.gitignore | 1 +
services/groupware/Makefile | 13 +-
services/groupware/apidoc.yml | 16 +-
services/groupware/package.json | 10 +-
services/groupware/pkg/groupware/.gitignore | 1 +
.../pkg/groupware/groupware_api_account.go | 40 +-
.../pkg/groupware/groupware_api_blob.go | 24 +-
.../pkg/groupware/groupware_api_calendars.go | 28 +-
.../pkg/groupware/groupware_api_contacts.go | 28 +-
.../pkg/groupware/groupware_api_emails.go | 371 ++++++----
.../pkg/groupware/groupware_api_identity.go | 17 +-
.../pkg/groupware/groupware_api_index.go | 6 +-
.../pkg/groupware/groupware_api_mailbox.go | 130 ++--
.../pkg/groupware/groupware_api_quota.go | 12 +-
.../pkg/groupware/groupware_api_tasklists.go | 14 +-
.../pkg/groupware/groupware_error.go | 40 +-
.../pkg/groupware/groupware_examples_test.go | 219 ++++++
.../pkg/groupware/groupware_request.go | 76 +-
.../pkg/groupware/groupware_response.go | 2 +-
.../pkg/groupware/groupware_route.go | 26 +-
services/groupware/pkg/middleware/auth.go | 26 +-
.../pkg/middleware/groupware_logger.go | 10 +-
services/groupware/pnpm-lock.yaml | 638 +++++-----------
31 files changed, 1701 insertions(+), 780 deletions(-)
create mode 100644 pkg/jmap/.gitignore
create mode 100644 pkg/jmap/jmap_model_examples.go
create mode 100644 pkg/jmap/jmap_model_examples_test.go
create mode 100644 pkg/jscalendar/.gitignore
create mode 100644 pkg/jscontact/.gitignore
create mode 100644 services/groupware/pkg/groupware/.gitignore
create mode 100644 services/groupware/pkg/groupware/groupware_examples_test.go
diff --git a/pkg/jmap/.gitignore b/pkg/jmap/.gitignore
new file mode 100644
index 0000000000..89ae54d4b9
--- /dev/null
+++ b/pkg/jmap/.gitignore
@@ -0,0 +1 @@
+/apidoc-examples.json
diff --git a/pkg/jmap/jmap_api_quota.go b/pkg/jmap/jmap_api_quota.go
index 14ba53dc83..3b82a13bd9 100644
--- a/pkg/jmap/jmap_api_quota.go
+++ b/pkg/jmap/jmap_api_quota.go
@@ -14,7 +14,7 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co
invocations := make([]Invocation, len(uniqueAccountIds))
for i, accountId := range uniqueAccountIds {
- invocations[i] = invocation(CommandQuotaGet, MailboxQueryCommand{AccountId: accountId}, mcid(accountId, "0"))
+ invocations[i] = invocation(CommandQuotaGet, QuotaGetCommand{AccountId: accountId}, mcid(accountId, "0"))
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 3863b61bf8..49b4b7a96a 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -134,6 +134,7 @@ const (
CalendarEventType = ObjectType("CalendarEvent")
CalendarEventNotificationType = ObjectType("CalendarEventNotification")
ParticipantIdentityType = ObjectType("ParticipantIdentity")
+ FileNodeType = ObjectType("FileNode")
JmapKeywordPrefix = "$"
JmapKeywordSeen = "$seen"
@@ -2116,7 +2117,11 @@ const (
EmailPropertyHtmlBody = "htmlBody"
EmailPropertyAttachments = "attachments"
EmailPropertyHasAttachment = "hasAttachment"
+ EmailPropertyHasKeyword = "hasKeyword"
EmailPropertyPreview = "preview"
+
+ EmailSortPropertyAllInThreadHaveKeyword = "allInThreadHaveKeyword"
+ EmailSortPropertySomeInThreadHaveKeyword = "someInThreadHaveKeyword"
)
var EmailProperties = []string{
@@ -3317,12 +3322,13 @@ type IdentitySetResponse struct {
NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"`
}
+// An Identity object stores information about an email address or domain the user may send from.
type Identity struct {
// The id of the Identity.
- Id string `json:"id,omitempty"`
+ Id string `json:"id,omitempty" doc:"!request"`
// The “From” name the client SHOULD use when creating a new Email from this Identity.
- Name string `json:"name,omitempty"`
+ Name string `json:"name,omitempty" doc:"req"`
// The “From” email address the client MUST use when creating a new Email from this Identity.
//
@@ -4879,7 +4885,7 @@ type Shareable struct {
// It then shows as well the current usage in regard to that limit.
type Quota struct {
// The unique identifier for this object.
- Id string `json:"id"`
+ Id string `json:"id" doc:"!request"`
// The resource type of the quota.
ResourceType ResourceType `json:"resourceType"`
@@ -4897,9 +4903,9 @@ type Quota struct {
// The Scope data type is used to represent the entities the quota applies to.
//
// It is defined as a "String" with values from the following set:
- // !- `account`: The quota information applies to just the client's account.
- // !- `domain`: The quota information applies to all accounts sharing this domain.
- // !- `global`: The quota information applies to all accounts belonging to the server.
+ // - `account`: The quota information applies to just the client's account.
+ // - `domain`: The quota information applies to all accounts sharing this domain.
+ // - `global`: The quota information applies to all accounts belonging to the server.
Scope Scope `json:"scope"`
// The name of the quota.
diff --git a/pkg/jmap/jmap_model_examples.go b/pkg/jmap/jmap_model_examples.go
new file mode 100644
index 0000000000..380ab3bd11
--- /dev/null
+++ b/pkg/jmap/jmap_model_examples.go
@@ -0,0 +1,694 @@
+//go:build groupware_examples
+
+package jmap
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "reflect"
+ "strings"
+)
+
+type Exampler struct {
+ AccountId string
+ SharedAccountId string
+ IdentityId string
+ IdentityName string
+ EmailAddress string
+ BccName string
+ BccAddress string
+ OtherIdentityId string
+ OtherIdentityName string
+ OtherIdentityEmailAddress string
+ SharedIdentityId string
+ SharedIdentityName string
+ SharedIdentityEmailAddress string
+ ThreadId string
+ EmailId string
+ EmailIds []string
+ QuotaId string
+ SharedQuotaId string
+ Username string
+ SharedAccountName string
+ TextSignature string
+ MailboxInboxId string
+ MailboxDraftsId string
+ MailboxSentId string
+ MailboxJunkId string
+ MailboxDeletedId string
+ MailboxProjectId string
+ SenderEmailAddress string
+ SenderName string
+}
+
+var ExamplerInstance = Exampler{
+ AccountId: "b",
+ Username: "cdrummer",
+ IdentityId: "aemua9ai",
+ IdentityName: "Camina Drummer",
+ EmailAddress: "cdrummer@opa.example.com",
+ BccName: "OPA Secretary",
+ BccAddress: "secretary@opa.example.com",
+ OtherIdentityId: "reogh7ia",
+ OtherIdentityName: "Transport Union President",
+ OtherIdentityEmailAddress: "pres@tu.example.com",
+ SharedAccountId: "s",
+ SharedAccountName: "OPA Leadership",
+ SharedIdentityId: "eeyie4qu",
+ SharedIdentityName: "OPA",
+ SharedIdentityEmailAddress: "bosmangs@opa.example.com",
+ ThreadId: "soh0aixi",
+ EmailId: "oot8eev2",
+ EmailIds: []string{"oot8eev2", "phah6ang", "fo7raidi", "kahsha6p"},
+ QuotaId: "iezuu7ah",
+ SharedQuotaId: "voos4oht",
+ TextSignature: strings.Join([]string{"Camina Drummer", "President of the Transport Union"}, "\n"),
+ MailboxInboxId: "a",
+ MailboxDraftsId: "d",
+ MailboxSentId: "e",
+ MailboxJunkId: "c",
+ MailboxDeletedId: "b",
+ MailboxProjectId: "i",
+ SenderEmailAddress: "klaes@opa.example.com",
+ SenderName: "Klaes Ashford",
+}
+
+func (e Exampler) SessionMailAccountCapabilities() SessionMailAccountCapabilities {
+ return SessionMailAccountCapabilities{
+ MaxMailboxesPerEmail: 0,
+ MaxMailboxDepth: 10,
+ MaxSizeMailboxName: 255,
+ MaxSizeAttachmentsPerEmail: 50000000,
+ EmailQuerySortOptions: []string{
+ EmailPropertyReceivedAt,
+ EmailPropertySize,
+ EmailPropertyFrom,
+ EmailPropertyTo,
+ EmailPropertySubject,
+ EmailPropertySentAt,
+ EmailPropertyHasAttachment,
+ EmailSortPropertyAllInThreadHaveKeyword,
+ EmailSortPropertySomeInThreadHaveKeyword,
+ },
+ MayCreateTopLevelMailbox: true,
+ }
+}
+
+func (e Exampler) SessionSubmissionAccountCapabilities() SessionSubmissionAccountCapabilities {
+ return SessionSubmissionAccountCapabilities{
+ MaxDelayedSend: 2592000,
+ SubmissionExtensions: map[string][]string{
+ "DELIVERYBY": {},
+ "DSN": {},
+ "FUTURERELEASE": {},
+ "MT-PRIORITY": {"MIXER"},
+ "REQUIRETLS": {},
+ "SIZE": {},
+ },
+ }
+}
+
+func (e Exampler) SessionVacationResponseAccountCapabilities() SessionVacationResponseAccountCapabilities {
+ return SessionVacationResponseAccountCapabilities{}
+}
+
+func (e Exampler) SessionSieveAccountCapabilities() SessionSieveAccountCapabilities {
+ return SessionSieveAccountCapabilities{
+ MaxSizeScriptName: 512,
+ MaxSizeScript: 1048576,
+ MaxNumberScripts: 100,
+ MaxNumberRedirects: 1,
+ SieveExtensions: []string{
+ "body",
+ "comparator-elbonia",
+ "comparator-i;ascii-casemap",
+ "comparator-i;ascii-numeric",
+ "comparator-i;octet",
+ "convert",
+ "copy",
+ "date",
+ "duplicate",
+ "editheader",
+ "enclose",
+ "encoded-character",
+ "enotify",
+ "envelope",
+ "envelope-deliverby",
+ "envelope-dsn",
+ "environment",
+ "ereject",
+ "extlists",
+ "extracttext",
+ "fcc",
+ "fileinto",
+ "foreverypart",
+ "ihave",
+ "imap4flags",
+ "imapsieve",
+ "include",
+ "index",
+ "mailbox",
+ "mailboxid",
+ "mboxmetadata",
+ "mime",
+ "redirect-deliverby",
+ "redirect-dsn",
+ "regex",
+ "reject",
+ "relational",
+ "replace",
+ "servermetadata",
+ "spamtest",
+ "spamtestplus",
+ "special-use",
+ "subaddress",
+ "vacation",
+ "vacation-seconds",
+ "variables",
+ "virustest",
+ },
+ NotificationMethods: []string{"mailto"},
+ ExternalLists: nil,
+ }
+}
+
+func (e Exampler) SessionBlobAccountCapabilities() SessionBlobAccountCapabilities {
+ return SessionBlobAccountCapabilities{
+ MaxSizeBlobSet: 7499488,
+ MaxDataSources: 16,
+ SupportedTypeNames: []string{"Email", "Thread", "SieveScript"},
+ SupportedDigestAlgorithms: []HttpDigestAlgorithm{
+ HttpDigestAlgorithmSha,
+ HttpDigestAlgorithmSha256,
+ HttpDigestAlgorithmSha512,
+ },
+ }
+}
+
+func (e Exampler) SessionQuotaAccountCapabilities() SessionQuotaAccountCapabilities {
+ return SessionQuotaAccountCapabilities{}
+}
+
+func (e Exampler) SessionContactsAccountCapabilities() SessionContactsAccountCapabilities {
+ return SessionContactsAccountCapabilities{
+ MaxAddressBooksPerCard: 128,
+ MayCreateAddressBook: true,
+ }
+}
+
+func (e Exampler) SessionCalendarsAccountCapabilities() SessionCalendarsAccountCapabilities {
+ var maxCals uint = 128
+ var maxParticipants uint = 20
+ minDate := UTCDate("0001-01-01T00:00:00Z")
+ maxDate := UTCDate("65534-12-31T23:59:59Z")
+ maxExp := Duration("P52W1D")
+ create := true
+ return SessionCalendarsAccountCapabilities{
+ MaxCalendarsPerEvent: &maxCals,
+ MinDateTime: &minDate,
+ MaxDateTime: &maxDate,
+ MaxExpandedQueryDuration: maxExp,
+ MaxParticipantsPerEvent: &maxParticipants,
+ MayCreateCalendar: &create,
+ }
+}
+
+func (e Exampler) SessionCalendarsParseAccountCapabilities() SessionCalendarsParseAccountCapabilities {
+ return SessionCalendarsParseAccountCapabilities{}
+}
+
+func (e Exampler) sessionPrincipalsAccountCapabilities(accountId string) SessionPrincipalsAccountCapabilities {
+ return SessionPrincipalsAccountCapabilities{
+ CurrentUserPrincipalId: accountId,
+ }
+}
+
+func (e Exampler) SessionPrincipalsAccountCapabilities() SessionPrincipalsAccountCapabilities {
+ return e.sessionPrincipalsAccountCapabilities(e.AccountId)
+}
+
+func (e Exampler) SessionPrincipalAvailabilityAccountCapabilities() SessionPrincipalAvailabilityAccountCapabilities {
+ return SessionPrincipalAvailabilityAccountCapabilities{
+ MaxAvailabilityDuration: Duration("P52W1D"),
+ }
+}
+
+func (e Exampler) SessionTasksAccountCapabilities() SessionTasksAccountCapabilities {
+ return SessionTasksAccountCapabilities{
+ MinDateTime: LocalDate("0001-01-01T00:00:00Z"),
+ MaxDateTime: LocalDate("65534-12-31T23:59:59Z"),
+ MayCreateTaskList: true,
+ }
+}
+
+func (e Exampler) SessionTasksAlertsAccountCapabilities() SessionTasksAlertsAccountCapabilities {
+ return SessionTasksAlertsAccountCapabilities{}
+}
+
+func (e Exampler) SessionTasksAssigneesAccountCapabilities() SessionTasksAssigneesAccountCapabilities {
+ return SessionTasksAssigneesAccountCapabilities{}
+}
+
+func (e Exampler) SessionTasksRecurrencesAccountCapabilities() SessionTasksRecurrencesAccountCapabilities {
+ return SessionTasksRecurrencesAccountCapabilities{
+ MaxExpandedQueryDuration: Duration("P260W1D"),
+ }
+}
+
+func (e Exampler) SessionTasksMultilingualAccountCapabilities() SessionTasksMultilingualAccountCapabilities {
+ return SessionTasksMultilingualAccountCapabilities{}
+}
+
+func (e Exampler) SessionTasksCustomTimezonesAccountCapabilities() SessionTasksCustomTimezonesAccountCapabilities {
+ return SessionTasksCustomTimezonesAccountCapabilities{}
+}
+
+func (e Exampler) SessionPrincipalsOwnerAccountCapabilities() SessionPrincipalsOwnerAccountCapabilities {
+ return SessionPrincipalsOwnerAccountCapabilities{
+ AccountIdForPrincipal: e.AccountId,
+ PrincipalId: e.AccountId,
+ }
+}
+
+func (e Exampler) SessionMDNAccountCapabilities() SessionMDNAccountCapabilities {
+ return SessionMDNAccountCapabilities{}
+}
+
+func (e Exampler) SessionAccountCapabilities() SessionAccountCapabilities {
+ return e.sessionAccountCapabilities(e.AccountId)
+}
+
+func (e Exampler) sessionAccountCapabilities(accountId string) SessionAccountCapabilities {
+ mail := e.SessionMailAccountCapabilities()
+ submission := e.SessionSubmissionAccountCapabilities()
+ vacationResponse := e.SessionVacationResponseAccountCapabilities()
+ sieve := e.SessionSieveAccountCapabilities()
+ blob := e.SessionBlobAccountCapabilities()
+ quota := e.SessionQuotaAccountCapabilities()
+ contacts := e.SessionContactsAccountCapabilities()
+ calendars := e.SessionCalendarsAccountCapabilities()
+ calendarsParse := e.SessionCalendarsParseAccountCapabilities()
+ principals := e.sessionPrincipalsAccountCapabilities(accountId)
+ principalsAvailability := e.SessionPrincipalAvailabilityAccountCapabilities()
+ tasks := e.SessionTasksAccountCapabilities()
+ tasksAlerts := e.SessionTasksAlertsAccountCapabilities()
+ tasksAssignees := e.SessionTasksAssigneesAccountCapabilities()
+ tasksRecurrences := e.SessionTasksRecurrencesAccountCapabilities()
+ tasksMultilingual := e.SessionTasksMultilingualAccountCapabilities()
+ tasksCustomTimezones := e.SessionTasksCustomTimezonesAccountCapabilities()
+ principalsOwner := e.SessionPrincipalsOwnerAccountCapabilities()
+ mdn := e.SessionMDNAccountCapabilities()
+ return SessionAccountCapabilities{
+ Mail: &mail,
+ Submission: &submission,
+ VacationResponse: &vacationResponse,
+ Sieve: &sieve,
+ Blob: &blob,
+ Quota: "a,
+ Contacts: &contacts,
+ Calendars: &calendars,
+ CalendarsParse: &calendarsParse,
+ Principals: &principals,
+ PrincipalsAvailability: &principalsAvailability,
+ Tasks: &tasks,
+ TasksAlerts: &tasksAlerts,
+ TasksAssignees: &tasksAssignees,
+ TasksRecurrences: &tasksRecurrences,
+ TasksMultilingual: &tasksMultilingual,
+ TasksCustomTimezones: &tasksCustomTimezones,
+ PrincipalsOwner: &principalsOwner,
+ MDN: &mdn,
+ }
+}
+
+func (e Exampler) Account() (Account, string) {
+ return Account{
+ Name: e.Username,
+ IsPersonal: true,
+ IsReadOnly: false,
+ AccountCapabilities: e.SessionAccountCapabilities(),
+ }, "A personal account"
+}
+
+func (e Exampler) SharedAccount() (Account, string, string) {
+ return Account{
+ Name: e.SharedAccountId,
+ IsPersonal: false,
+ IsReadOnly: true,
+ AccountCapabilities: e.sessionAccountCapabilities(e.SharedAccountId),
+ }, "A read-only shared account", "shared"
+}
+
+func (e Exampler) Accounts() []Account {
+ a, _ := e.Account()
+ s, _, _ := e.SharedAccount()
+ return []Account{a, s}
+}
+
+func (e Exampler) Quota() Quota {
+ return Quota{
+ Id: e.QuotaId,
+ ResourceType: "octets",
+ Scope: "account",
+ Used: 11696865,
+ HardLimit: 20000000000,
+ Name: e.Username,
+ Types: []ObjectType{
+ EmailType,
+ SieveScriptType,
+ FileNodeType,
+ CalendarEventType,
+ ContactCardType,
+ },
+ Description: e.IdentityName,
+ SoftLimit: 19000000000,
+ WarnLimit: 10000000000,
+ }
+}
+
+func (e Exampler) Quotas() []Quota {
+ return []Quota{
+ e.Quota(),
+ {
+ Id: e.SharedQuotaId,
+ ResourceType: "octets",
+ Scope: "account",
+ Used: 29102918,
+ HardLimit: 50000000000,
+ Name: e.SharedAccountId,
+ Types: []ObjectType{
+ EmailType,
+ SieveScriptType,
+ FileNodeType,
+ CalendarEventType,
+ ContactCardType,
+ },
+ Description: e.SharedAccountName,
+ SoftLimit: 90000000000,
+ WarnLimit: 100000000000,
+ },
+ }
+}
+
+func (e Exampler) Identity() Identity {
+ return Identity{
+ Id: e.IdentityId,
+ Name: e.IdentityName,
+ Email: e.EmailAddress,
+ Bcc: &[]EmailAddress{
+ {Name: e.BccName, Email: e.BccAddress},
+ },
+ MayDelete: true,
+ TextSignature: &e.TextSignature,
+ }
+}
+
+func (e Exampler) OtherIdentity() (Identity, string, string) {
+ return Identity{
+ Id: e.OtherIdentityId,
+ Name: e.OtherIdentityName,
+ Email: e.OtherIdentityEmailAddress,
+ MayDelete: false,
+ }, "Another Identity", "other"
+}
+
+func (e Exampler) Identities() []Identity {
+ a := e.Identity()
+ b, _, _ := e.OtherIdentity()
+ return []Identity{a, b}
+}
+
+func (e Exampler) Identity_req() Identity {
+ return Identity{
+ Name: e.IdentityName,
+ Email: e.EmailAddress,
+ Bcc: &[]EmailAddress{
+ {Name: e.BccName, Email: e.BccAddress},
+ },
+ TextSignature: &e.TextSignature,
+ }
+}
+
+func (e Exampler) Thread() Thread {
+ return Thread{
+ Id: e.ThreadId,
+ EmailIds: e.EmailIds,
+ }
+}
+
+func (e Exampler) MailboxInbox() (Mailbox, string, string) {
+ return Mailbox{
+ Id: e.MailboxInboxId,
+ Name: "Inbox",
+ Role: JmapMailboxRoleInbox,
+ SortOrder: intPtr(0),
+ TotalEmails: 1291,
+ UnreadEmails: 82,
+ TotalThreads: 891,
+ UnreadThreads: 55,
+ MyRights: &MailboxRights{
+ MayReadItems: true,
+ MayAddItems: true,
+ MayRemoveItems: true,
+ MaySetSeen: true,
+ MaySetKeywords: true,
+ MayCreateChild: true,
+ MayRename: true,
+ MayDelete: true,
+ MaySubmit: true,
+ },
+ IsSubscribed: boolPtr(true),
+ }, "An Inbox Mailbox", "inbox"
+}
+
+func (e Exampler) MailboxInboxProjects() (Mailbox, string, string) {
+ return Mailbox{
+ Id: e.MailboxProjectId,
+ ParentId: e.MailboxInboxId,
+ Name: "Projects",
+ SortOrder: intPtr(0),
+ TotalEmails: 112,
+ UnreadEmails: 3,
+ TotalThreads: 85,
+ UnreadThreads: 2,
+ MyRights: &MailboxRights{
+ MayReadItems: true,
+ MayAddItems: true,
+ MayRemoveItems: true,
+ MaySetSeen: true,
+ MaySetKeywords: true,
+ MayCreateChild: true,
+ MayRename: true,
+ MayDelete: true,
+ MaySubmit: true,
+ },
+ IsSubscribed: boolPtr(true),
+ }, "A Projects Mailbox under the Inbox", "projects"
+}
+
+func (e Exampler) MailboxDrafts() (Mailbox, string, string) {
+ return Mailbox{
+ Id: e.MailboxDraftsId,
+ Name: "Drafts",
+ Role: JmapMailboxRoleDrafts,
+ SortOrder: intPtr(0),
+ TotalEmails: 12,
+ UnreadEmails: 1,
+ TotalThreads: 12,
+ UnreadThreads: 1,
+ MyRights: &MailboxRights{
+ MayReadItems: true,
+ MayAddItems: true,
+ MayRemoveItems: true,
+ MaySetSeen: true,
+ MaySetKeywords: true,
+ MayCreateChild: true,
+ MayRename: true,
+ MayDelete: true,
+ MaySubmit: true,
+ },
+ IsSubscribed: boolPtr(true),
+ }, "A Drafts Mailbox", "drafts"
+}
+
+func (e Exampler) MailboxSent() (Mailbox, string, string) {
+ return Mailbox{
+ Id: e.MailboxSentId,
+ Name: "Sent Items",
+ Role: JmapMailboxRoleSent,
+ SortOrder: intPtr(0),
+ TotalEmails: 1621,
+ UnreadEmails: 0,
+ TotalThreads: 1621,
+ UnreadThreads: 0,
+ MyRights: &MailboxRights{
+ MayReadItems: true,
+ MayAddItems: true,
+ MayRemoveItems: true,
+ MaySetSeen: true,
+ MaySetKeywords: true,
+ MayCreateChild: true,
+ MayRename: true,
+ MayDelete: true,
+ MaySubmit: true,
+ },
+ IsSubscribed: boolPtr(true),
+ }, "A Sent Mailbox", "sent"
+}
+
+func (e Exampler) MailboxJunk() (Mailbox, string, string) {
+ return Mailbox{
+ Id: e.MailboxJunkId,
+ Name: "Junk Mail",
+ Role: JmapMailboxRoleJunk,
+ SortOrder: intPtr(0),
+ TotalEmails: 251,
+ UnreadEmails: 0,
+ TotalThreads: 251,
+ UnreadThreads: 0,
+ MyRights: &MailboxRights{
+ MayReadItems: true,
+ MayAddItems: true,
+ MayRemoveItems: true,
+ MaySetSeen: true,
+ MaySetKeywords: true,
+ MayCreateChild: true,
+ MayRename: true,
+ MayDelete: true,
+ MaySubmit: true,
+ },
+ IsSubscribed: boolPtr(true),
+ }, "A Junk Mailbox", "junk"
+}
+
+func (e Exampler) MailboxDeleted() (Mailbox, string, string) {
+ return Mailbox{
+ Id: e.MailboxDeletedId,
+ Name: "Deleted Items",
+ Role: JmapMailboxRoleTrash,
+ SortOrder: intPtr(0),
+ TotalEmails: 99,
+ UnreadEmails: 0,
+ TotalThreads: 91,
+ UnreadThreads: 0,
+ MyRights: &MailboxRights{
+ MayReadItems: true,
+ MayAddItems: true,
+ MayRemoveItems: true,
+ MaySetSeen: true,
+ MaySetKeywords: true,
+ MayCreateChild: true,
+ MayRename: true,
+ MayDelete: true,
+ MaySubmit: true,
+ },
+ IsSubscribed: boolPtr(true),
+ }, "A Trash Mailbox", "deleted"
+}
+
+func (e Exampler) Mailboxes() []Mailbox {
+ a, _, _ := e.MailboxInbox()
+ b, _, _ := e.MailboxDrafts()
+ c, _, _ := e.MailboxSent()
+ d, _, _ := e.MailboxJunk()
+ f, _, _ := e.MailboxDeleted()
+ g, _, _ := e.MailboxInboxProjects()
+ return []Mailbox{a, b, c, d, f, g}
+}
+
+func SerializeExamples(e any) {
+ type example struct {
+ Type string `json:"type"`
+ Key string `json:"key,omitempty"`
+ Title string `json:"title,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ Origin string `json:"origin,omitempty"`
+ Example any `json:"example"`
+ }
+
+ filename := os.Getenv("EXAMPLE_OUTPUT_FILE")
+ if filename == "" {
+ filename = "apidoc-examples.json"
+ }
+
+ funcs := map[string]func() (example, error){}
+ reflected := reflect.ValueOf(e)
+ r := reflect.TypeOf(e)
+ conflicts := map[string]example{}
+ for i := 0; i < r.NumMethod(); i++ {
+ name := r.Method(i).Name
+ m := reflected.MethodByName(name)
+ funcs[name] = func() (example, error) {
+ results := m.Call(nil)
+ title := ""
+ key := "default"
+ typ := ""
+ uniqueId := ""
+ var result reflect.Value
+ switch len(results) {
+ case 1:
+ result = results[0]
+ case 2:
+ result = results[0]
+ title = results[1].String()
+ case 3:
+ result = results[0]
+ title = results[1].String()
+ key = results[2].String()
+ case 4:
+ result = results[0]
+ title = results[1].String()
+ key = results[2].String()
+ typ = results[3].String()
+ default:
+ return example{}, fmt.Errorf("method result does not have 1 or 2 or 3 or 4 results but %d", len(results))
+ }
+ t := result.Type()
+ scope := "" // same as "any"
+ if strings.HasSuffix(name, "_req") {
+ scope = "request"
+ }
+ origin := fmt.Sprintf("%s:%s", r.String(), name)
+
+ if typ == "" {
+ typ = t.String()
+ }
+
+ if uniqueId == "" {
+ uniqueId = typ + "." + key
+ }
+ conflictKey := uniqueId + "/" + scope
+ if conflict, ok := conflicts[conflictKey]; ok {
+ panic(fmt.Errorf("conflicting examples with the same unique identifier '%s', consider adding a key to either of '%s' or '%s'", conflictKey, conflict.Origin, origin))
+ }
+
+ ex := example{
+ Type: typ,
+ Key: uniqueId,
+ Title: title,
+ Scope: scope,
+ Origin: origin,
+ Example: result.Interface(),
+ }
+ conflicts[conflictKey] = ex
+ return ex, nil
+ }
+ }
+
+ examples := []example{}
+ for name, f := range funcs {
+ if ex, err := f(); err != nil {
+ panic(fmt.Errorf("the example producing method '%s' produced an error: %w", name, err))
+ } else {
+ examples = append(examples, ex)
+ }
+ }
+ if b, err := json.MarshalIndent(examples, "", " "); err != nil {
+ panic(fmt.Errorf("failed to serialize to JSON: %w", err))
+ } else {
+ if err := os.WriteFile(filename, b, 0644); err != nil {
+ panic(fmt.Errorf("failed to write the serialized JSON output to the file '%s': %w", filename, err))
+ }
+ }
+}
diff --git a/pkg/jmap/jmap_model_examples_test.go b/pkg/jmap/jmap_model_examples_test.go
new file mode 100644
index 0000000000..00f3c83d75
--- /dev/null
+++ b/pkg/jmap/jmap_model_examples_test.go
@@ -0,0 +1,8 @@
+//go:build groupware_examples
+
+package jmap
+
+func Example() {
+ SerializeExamples(ExamplerInstance)
+ //Output:
+}
diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go
index 6dd2f9c50b..c6f2f836e7 100644
--- a/pkg/jmap/jmap_tools.go
+++ b/pkg/jmap/jmap_tools.go
@@ -334,3 +334,11 @@ func mapPairs[K comparable, L, R any](left map[K]L, right map[K]R) map[K]pair[L,
}
return result
}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+func boolPtr(b bool) *bool {
+ return &b
+}
diff --git a/pkg/jscalendar/.gitignore b/pkg/jscalendar/.gitignore
new file mode 100644
index 0000000000..89ae54d4b9
--- /dev/null
+++ b/pkg/jscalendar/.gitignore
@@ -0,0 +1 @@
+/apidoc-examples.json
diff --git a/pkg/jscontact/.gitignore b/pkg/jscontact/.gitignore
new file mode 100644
index 0000000000..89ae54d4b9
--- /dev/null
+++ b/pkg/jscontact/.gitignore
@@ -0,0 +1 @@
+/apidoc-examples.json
diff --git a/services/groupware/.gitignore b/services/groupware/.gitignore
index 9989a9005f..36cb322bf5 100644
--- a/services/groupware/.gitignore
+++ b/services/groupware/.gitignore
@@ -1,4 +1,5 @@
/swagger.yml
+/openapi.yml
/api.html
/api.html.template
/node_modules
diff --git a/services/groupware/Makefile b/services/groupware/Makefile
index 571ebe98af..fbd81785e4 100644
--- a/services/groupware/Makefile
+++ b/services/groupware/Makefile
@@ -21,8 +21,10 @@ node_modules:
pnpm install
.PHONY: swagger.yml
-swagger.yml: apidoc.yml tsnode
- swagger generate spec --include='groupware' --include='jmap' --include='jscalendar' --include='jscontact' --scan-models --input=$< | NODE_OPTIONS='--no-warnings --loader ts-node/esm' pnpm exec ts-node apidoc-process.ts > $@
+swagger.yml: apidoc.yml tsnode openapi.yml
+ #swagger generate spec --include='groupware' --include='jmap' --include='jscalendar' --include='jscontact' --scan-models --input=$< | NODE_OPTIONS='--no-warnings --loader ts-node/esm' pnpm exec ts-node apidoc-process.ts > $@
+ #NODE_OPTIONS='--no-warnings --loader ts-node/esm' pnpm exec ts-node apidoc-process.ts > $@ < openapi.yml
+ cp openapi.yml $@
APIDOC_PORT=9999
@@ -37,3 +39,10 @@ api.html: swagger.yml favicon.png tsnode
.PHONY: apidoc-static
apidoc-static: api.html
+
+.PHONY: examples
+examples:
+ cd ../../pkg/jmap/ && go test -tags=groupware_examples . -v -count=1 -run Example
+ cd ../../pkg/jscontact/ && go test -tags=groupware_examples . -v -count=1 -run Example
+ cd ../../pkg/jscalendar/ && go test -tags=groupware_examples . -v -count=1 -run Example
+ cd ./pkg/groupware/ && go test -tags=groupware_examples . -v -count=1 -run Example
diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml
index 0becebd89e..42da082b2a 100644
--- a/services/groupware/apidoc.yml
+++ b/services/groupware/apidoc.yml
@@ -1,7 +1,11 @@
+openapi: 3.0.4
servers:
- url: https://localhost:9200/
description: Local Development Server
tags:
+ - name: untagged
+ x-displayName: Uncategorized
+ description: APIs that are not categorized yet
- name: bootstrap
x-displayName: Bootstrapping
description: Initialization APIs
@@ -37,7 +41,14 @@ tags:
description: APIs about tasks
- name: quota
x-displayName: Quota
- description: APIs about quotas
+ description: |-
+ Quotas are objects that display the limits set to an account usage.
+
+ They also indicate the current usage in regard to those limits.
+
+ JMAP Quotas are defined in [RFC9524](https://www.rfc-editor.org/rfc/rfc9425.html).
+
+ This sectiong roups the APIs that pertain to retrieving and modifying quotas.
- name: vacation
x-displayName: Vacation Responses
description: APIs about vacation responses
@@ -69,6 +80,9 @@ x-tagGroups:
- name: Quotas
tags:
- quota
+ - name: Uncategorized
+ tags:
+ - untagged
components:
securitySchemes:
api:
diff --git a/services/groupware/package.json b/services/groupware/package.json
index ddeb4cc960..7054ca24d3 100644
--- a/services/groupware/package.json
+++ b/services/groupware/package.json
@@ -1,15 +1,15 @@
{
"dependencies": {
- "@redocly/cli": "^2.4.0",
+ "@redocly/cli": "^2.14.5",
"@types/js-yaml": "^4.0.9",
"cheerio": "^1.1.2",
- "js-yaml": "^4.1.0",
+ "js-yaml": "^4.1.1",
"ts-node": "^10.9.2",
- "typescript": "^5.9.2"
+ "typescript": "^5.9.3"
},
- "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
+ "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
"type": "module",
"devDependencies": {
- "@types/node": "^24.3.1"
+ "@types/node": "^24.10.7"
}
}
diff --git a/services/groupware/pkg/groupware/.gitignore b/services/groupware/pkg/groupware/.gitignore
new file mode 100644
index 0000000000..89ae54d4b9
--- /dev/null
+++ b/services/groupware/pkg/groupware/.gitignore
@@ -0,0 +1 @@
+/apidoc-examples.json
diff --git a/services/groupware/pkg/groupware/groupware_api_account.go b/services/groupware/pkg/groupware/groupware_api_account.go
index 52b12aa94d..94927c8872 100644
--- a/services/groupware/pkg/groupware/groupware_api_account.go
+++ b/services/groupware/pkg/groupware/groupware_api_account.go
@@ -33,7 +33,8 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
if err != nil {
return errorResponse(single(accountId), err)
}
- return etagResponse(single(accountId), account, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
+ var body jmap.Account = account
+ return etagResponse(single(accountId), body, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
})
}
@@ -66,7 +67,8 @@ func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) {
}
// sort on accountId to have a stable order that remains the same with every query
slices.SortFunc(list, func(a, b AccountWithId) int { return strings.Compare(a.AccountId, b.AccountId) })
- return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), list, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
+ var RBODY []AccountWithId = list
+ return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
})
}
@@ -94,7 +96,8 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt
}
// sort on accountId to have a stable order that remains the same with every query
slices.SortFunc(list, func(a, b AccountWithIdAndIdentities) int { return strings.Compare(a.AccountId, b.AccountId) })
- return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), list, sessionState, AccountResponseObjectType, state, lang)
+ var RBODY []AccountWithIdAndIdentities = list
+ return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state, lang)
})
}
@@ -108,34 +111,3 @@ type AccountWithIdAndIdentities struct {
jmap.Account
Identities []jmap.Identity `json:"identities,omitempty"`
}
-
-type AccountBootstrapResponse struct {
- // The API version.
- Version string `json:"version"`
-
- // A list of capabilities of this API version.
- Capabilities []string `json:"capabilities"`
-
- // API limits.
- Limits IndexLimits `json:"limits"`
-
- // Accounts that are available to the user.
- //
- // The key of the mapis the identifier.
- Accounts map[string]IndexAccount `json:"accounts"`
-
- // Primary accounts for usage types.
- PrimaryAccounts IndexPrimaryAccounts `json:"primaryAccounts"`
-
- // Mailboxes.
- Mailboxes map[string][]jmap.Mailbox `json:"mailboxes"`
-}
-
-// When the request suceeds.
-// swagger:response GetAccountBootstrapResponse200
-type SwaggerAccountBootstrapResponse struct {
- // in: body
- Body struct {
- *AccountBootstrapResponse
- }
-}
diff --git a/services/groupware/pkg/groupware/groupware_api_blob.go b/services/groupware/pkg/groupware/groupware_api_blob.go
index b733abf136..e4975825bc 100644
--- a/services/groupware/pkg/groupware/groupware_api_blob.go
+++ b/services/groupware/pkg/groupware/groupware_api_blob.go
@@ -1,12 +1,10 @@
package groupware
import (
- "fmt"
"io"
"net/http"
"strconv"
- "github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/log"
)
@@ -22,9 +20,9 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
}
l := req.logger.With().Str(logAccountId, accountId)
- blobId := chi.URLParam(req.r, UriParamBlobId)
- if blobId == "" {
- return req.parameterErrorResponse(single(accountId), UriParamBlobId, fmt.Sprintf("Invalid value for path parameter '%v': empty", UriParamBlobId))
+ blobId, err := req.PathParam(UriParamBlobId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
}
l = l.Str(UriParamBlobId, blobId)
@@ -34,8 +32,7 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
}
- blob := res
- if blob == nil {
+ if res == nil {
return notFoundResponse(single(accountId), sessionState)
}
return etagResponse(single(accountId), res, sessionState, BlobResponseObjectType, state, lang)
@@ -72,10 +69,15 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
- blobId := chi.URLParam(req.r, UriParamBlobId)
- name := chi.URLParam(req.r, UriParamBlobName)
- q := req.r.URL.Query()
- typ := q.Get(QueryParamBlobType)
+ blobId, err := req.PathParam(UriParamBlobId)
+ if err != nil {
+ return err
+ }
+ name, err := req.PathParam(UriParamBlobName)
+ if err != nil {
+ return err
+ }
+ typ, _ := req.getStringParam(QueryParamBlobType, "")
accountId, gwerr := req.GetAccountIdForBlob()
if gwerr != nil {
diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go
index 8524066d26..0cca84752e 100644
--- a/services/groupware/pkg/groupware/groupware_api_calendars.go
+++ b/services/groupware/pkg/groupware/groupware_api_calendars.go
@@ -4,7 +4,6 @@ import (
"net/http"
"strings"
- "github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
@@ -68,7 +67,10 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
- calendarId := chi.URLParam(r, UriParamCalendarId)
+ calendarId, err := req.PathParam(UriParamCalendarId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
logger := log.From(l)
@@ -110,7 +112,10 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
l := req.logger.With()
- calendarId := chi.URLParam(r, UriParamCalendarId)
+ calendarId, err := req.PathParam(UriParamCalendarId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
@@ -157,9 +162,6 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request)
l := req.logger.With()
- calendarId := chi.URLParam(r, UriParamCalendarId)
- l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
-
var create jmap.CalendarEvent
err := req.body(&create)
if err != nil {
@@ -175,6 +177,7 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request)
})
}
+// @api:tag XYZ
func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
@@ -183,9 +186,11 @@ func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request)
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
- calendarId := chi.URLParam(r, UriParamCalendarId)
- eventId := chi.URLParam(r, UriParamEventId)
- l.Str(UriParamCalendarId, log.SafeString(calendarId)).Str(UriParamEventId, log.SafeString(eventId))
+ eventId, err := req.PathParam(UriParamEventId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l.Str(UriParamEventId, log.SafeString(eventId))
logger := log.From(l)
@@ -220,7 +225,10 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
return errorResponse(single(accountId), err)
}
- blobId := chi.URLParam(r, UriParamBlobId)
+ blobId, err := req.PathParam(UriParamBlobId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
blobIds := strings.Split(blobId, ",")
l := req.logger.With().Array(UriParamBlobId, log.SafeStringArray(blobIds))
diff --git a/services/groupware/pkg/groupware/groupware_api_contacts.go b/services/groupware/pkg/groupware/groupware_api_contacts.go
index 9cebd7ed58..cffcfd0126 100644
--- a/services/groupware/pkg/groupware/groupware_api_contacts.go
+++ b/services/groupware/pkg/groupware/groupware_api_contacts.go
@@ -3,7 +3,6 @@ package groupware
import (
"net/http"
- "github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/jscontact"
"github.com/opencloud-eu/opencloud/pkg/log"
@@ -68,7 +67,10 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
- addressBookId := chi.URLParam(r, UriParamAddressBookId)
+ addressBookId, err := req.PathParam(UriParamAddressBookId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
logger := log.From(l)
@@ -110,7 +112,10 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
l := req.logger.With()
- addressBookId := chi.URLParam(r, UriParamAddressBookId)
+ addressBookId, err := req.PathParam(UriParamAddressBookId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
@@ -157,7 +162,10 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
- contactId := chi.URLParam(r, UriParamContactId)
+ contactId, err := req.PathParam(UriParamContactId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l = l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
@@ -183,11 +191,14 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
- addressBookId := chi.URLParam(r, UriParamAddressBookId)
+ addressBookId, err := req.PathParam(UriParamAddressBookId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
var create jscontact.ContactCard
- err := req.body(&create)
+ err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set")
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -209,7 +220,10 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
- contactId := chi.URLParam(r, UriParamContactId)
+ contactId, err := req.PathParam(UriParamContactId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go
index 454831f9ff..058dad1e04 100644
--- a/services/groupware/pkg/groupware/groupware_api_emails.go
+++ b/services/groupware/pkg/groupware/groupware_api_emails.go
@@ -11,7 +11,6 @@ import (
"strings"
"time"
- "github.com/go-chi/chi/v5"
"github.com/microcosm-cc/bluemonday"
"github.com/rs/zerolog"
@@ -39,6 +38,54 @@ type SwaggerGetAllEmailsInMailboxSince200 struct {
}
}
+// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails/since/{since} email get_all_emails_in_mailbox_since
+// Get all the emails in a mailbox since a given state.
+//
+// Retrieve the list of all the emails that are in a given mailbox since a given state.
+//
+// The mailbox must be specified by its id, as part of the request URL path.
+//
+// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
+// respectively.
+//
+// responses:
+//
+// 200: GetAllEmailsInMailboxSince200
+// 400: ErrorResponse400
+// 404: ErrorResponse404
+// 500: ErrorResponse500
+func (g *Groupware) GetAllEmailsInMailboxSince(w http.ResponseWriter, r *http.Request) {
+
+ maxChanges := uint(0)
+ g.respond(w, r, func(req Request) Response {
+
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+
+ mailboxId, err := req.PathParam(UriParamMailboxId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+
+ since, err := req.PathParamDoc(UriParamSince, "State identifier that indicates the coordinate from whence on to list mailbox changes")
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+
+ logger := log.From(req.logger.With().Str(HeaderParamSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
+
+ changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges)
+ if jerr != nil {
+ return req.errorResponseFromJmap(single(accountId), jerr)
+ }
+
+ return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang)
+ })
+
+}
+
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails email get_all_emails_in_mailbox
// Get all the emails in a mailbox.
//
@@ -49,108 +96,79 @@ type SwaggerGetAllEmailsInMailboxSince200 struct {
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
// respectively.
//
-// When the query parameter 'since' or the 'if-none-match' header is specified, then the
-// request behaves differently, performing a changes query to determine what has changed in
-// that mailbox since a given state identifier.
-//
// responses:
//
-// 200: GetAllEmailsInMailbox200
-// 200: GetAllEmailsInMailboxSince200
-// 400: ErrorResponse400
-// 404: ErrorResponse404
-// 500: ErrorResponse500
+// 200: GetAllEmailsInMailbox200
+// 400: ErrorResponse400
+// 404: ErrorResponse404
+// 500: ErrorResponse500
func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) {
- mailboxId := chi.URLParam(r, UriParamMailboxId)
- since := r.Header.Get(HeaderSince)
+ g.respond(w, r, func(req Request) Response {
+ l := req.logger.With()
- if since != "" {
- // ... then it's a completely different operation
- maxChanges := uint(0)
- g.respond(w, r, func(req Request) Response {
- accountId, err := req.GetAccountIdForMail()
- if err != nil {
- return errorResponse(single(accountId), err)
- }
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l = l.Str(logAccountId, accountId)
- if mailboxId == "" {
- return req.parameterErrorResponse(single(accountId), UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
- }
+ mailboxId, err := req.PathParam(UriParamMailboxId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
- logger := log.From(req.logger.With().Str(HeaderSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
+ offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ if ok {
+ l = l.Int(QueryParamOffset, offset)
+ }
- changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges)
- if jerr != nil {
- return req.errorResponseFromJmap(single(accountId), jerr)
- }
+ limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ if ok {
+ l = l.Uint(QueryParamLimit, limit)
+ }
- return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang)
- })
- } else {
- g.respond(w, r, func(req Request) Response {
- l := req.logger.With()
+ logger := log.From(l)
- accountId, err := req.GetAccountIdForMail()
- if err != nil {
- return errorResponse(single(accountId), err)
- }
- l = l.Str(logAccountId, accountId)
+ collapseThreads := false
+ fetchBodies := false
+ withThreads := true
- if mailboxId == "" {
- return req.parameterErrorResponse(single(accountId), UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
- }
+ emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads)
+ if jerr != nil {
+ return req.errorResponseFromJmap(single(accountId), jerr)
+ }
- offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
- if err != nil {
- return errorResponse(single(accountId), err)
- }
- if ok {
- l = l.Int(QueryParamOffset, offset)
- }
+ sanitized, err := req.sanitizeEmails(emails.Emails)
+ if err != nil {
+ return errorResponseWithSessionState(single(accountId), err, sessionState)
+ }
- limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
- if err != nil {
- return errorResponse(single(accountId), err)
- }
- if ok {
- l = l.Uint(QueryParamLimit, limit)
- }
+ safe := jmap.Emails{
+ Emails: sanitized,
+ Total: emails.Total,
+ Limit: emails.Limit,
+ Offset: emails.Offset,
+ }
- logger := log.From(l)
-
- collapseThreads := false
- fetchBodies := false
- withThreads := true
-
- emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads)
- if jerr != nil {
- return req.errorResponseFromJmap(single(accountId), jerr)
- }
-
- sanitized, err := req.sanitizeEmails(emails.Emails)
- if err != nil {
- return errorResponseWithSessionState(single(accountId), err, sessionState)
- }
-
- safe := jmap.Emails{
- Emails: sanitized,
- Total: emails.Total,
- Limit: emails.Limit,
- Offset: emails.Offset,
- }
-
- return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang)
- })
- }
+ return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang)
+ })
}
func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, UriParamEmailId)
- ids := strings.Split(id, ",")
-
accept := r.Header.Get("Accept")
if accept == "message/rfc822" {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
+ id, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return err
+ }
+ ids := strings.Split(id, ",")
if len(ids) != 1 {
return req.parameterError(UriParamEmailId, fmt.Sprintf("when the Accept header is set to '%s', the API only supports serving a single email id", accept))
}
@@ -194,6 +212,11 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
+ id, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ ids := strings.Split(id, ",")
if len(ids) < 1 {
return req.parameterErrorResponse(single(accountId), UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamEmailId, log.SafeString(id), "empty list of mail ids"))
}
@@ -244,8 +267,6 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, UriParamEmailId)
-
contextAppender := func(l zerolog.Context) zerolog.Context { return l }
q := r.URL.Query()
var attachmentSelector func(jmap.EmailBodyPart) bool = nil
@@ -274,6 +295,12 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
return errorResponse(single(accountId), err)
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
+
+ id, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+
logger := log.From(l)
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
if jerr != nil {
@@ -286,20 +313,29 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
if err != nil {
return errorResponseWithSessionState(single(accountId), err, sessionState)
}
- return etagResponse(single(accountId), email.Attachments, sessionState, EmailResponseObjectType, state, lang)
+ var body []jmap.EmailBodyPart = email.Attachments
+ return etagResponse(single(accountId), body, sessionState, EmailResponseObjectType, state, lang)
})
} else {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
- mailAccountId, gwerr := req.GetAccountIdForMail()
- if gwerr != nil {
- return gwerr
+ mailAccountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return err
}
- blobAccountId, gwerr := req.GetAccountIdForBlob()
- if gwerr != nil {
- return gwerr
+ blobAccountId, err := req.GetAccountIdForBlob()
+ if err != nil {
+ return err
}
- l := req.logger.With().Str(logAccountId, log.SafeString(mailAccountId)).Str(logBlobAccountId, log.SafeString(blobAccountId))
+ id, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return err
+ }
+
+ l := req.logger.With().
+ Str(logAccountId, log.SafeString(mailAccountId)).
+ Str(logBlobAccountId, log.SafeString(blobAccountId)).
+ Str(UriParamEmailId, log.SafeString(id))
l = contextAppender(l)
logger := log.From(l)
@@ -434,25 +470,30 @@ type EmailSearchResults struct {
QueryState jmap.State `json:"queryState,omitempty"`
}
-func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) {
- q := req.r.URL.Query()
- mailboxId := q.Get(QueryParamMailboxId)
- notInMailboxIds := q[QueryParamNotInMailboxId]
- text := q.Get(QueryParamSearchText)
- from := q.Get(QueryParamSearchFrom)
- to := q.Get(QueryParamSearchTo)
- cc := q.Get(QueryParamSearchCc)
- bcc := q.Get(QueryParamSearchBcc)
- subject := q.Get(QueryParamSearchSubject)
- body := q.Get(QueryParamSearchBody)
- keywords := q[QueryParamSearchKeyword]
- messageId := q.Get(QueryParamSearchMessageId)
+func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) {
+ mailboxId, _ := req.getStringParam(QueryParamMailboxId, "") // the identifier of the Mailbox to which to restrict the search
+ text, _ := req.getStringParam(QueryParamSearchText, "") // text that must be included in the Email, specifically in From, To, Cc, Bcc, Subject and any text/* body part
+ from, _ := req.getStringParam(QueryParamSearchFrom, "") // text that must be included in the From header of the Email
+ to, _ := req.getStringParam(QueryParamSearchTo, "") // text that must be included in the To header of the Email
+ cc, _ := req.getStringParam(QueryParamSearchCc, "") // text that must be included in the Cc header of the Email
+ bcc, _ := req.getStringParam(QueryParamSearchBcc, "") // text that must be included in the Bcc header of the Email
+ subject, _ := req.getStringParam(QueryParamSearchSubject, "") // text that must be included in the Subject of the Email
+ body, _ := req.getStringParam(QueryParamSearchBody, "") // text that must be included in any text/* part of the body of the Email
+ messageId, _ := req.getStringParam(QueryParamSearchMessageId, "") // value of the Message-ID header of the Email
+ notInMailboxIds, _, err := req.parseOptStringListParam(QueryParamNotInMailboxId) // a comma-separated list of identifiers of Mailboxes the Email must *not* be in
+ if err != nil {
+ return false, nil, false, 0, 0, nil, err
+ }
+ keywords, _, err := req.parseOptStringListParam(QueryParamSearchKeyword) // the Email must have all those keywords
+ if err != nil {
+ return false, nil, false, 0, 0, nil, err
+ }
snippets := false
l := req.logger.With()
- offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
+ offset, ok, err := req.parseIntParam(QueryParamOffset, 0) // pagination element offset
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -460,7 +501,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Int(QueryParamOffset, offset)
}
- limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
+ limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit) // maximum number of results (size of a page)
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -468,7 +509,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Uint(QueryParamLimit, limit)
}
- before, ok, err := req.parseDateParam(QueryParamSearchBefore)
+ before, ok, err := req.parseDateParam(QueryParamSearchBefore) // the Email must have been received before this date-time
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -476,7 +517,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Time(QueryParamSearchBefore, before)
}
- after, ok, err := req.parseDateParam(QueryParamSearchAfter)
+ after, ok, err := req.parseDateParam(QueryParamSearchAfter) // the Email must have been received after this date-time
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -515,7 +556,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Str(QueryParamSearchMessageId, log.SafeString(messageId))
}
- minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0)
+ minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0) // the minimum size of the Email
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -523,7 +564,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Int(QueryParamSearchMinSize, minSize)
}
- maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0)
+ maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0) // the maximum size of the Email
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -586,7 +627,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
since := q.Get(QueryParamSince)
if since == "" {
- since = r.Header.Get(HeaderSince)
+ since = r.Header.Get(HeaderParamSince)
}
if since != "" {
// get email changes since a given state
@@ -599,7 +640,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) {
return errorResponse(single(accountId), err)
}
- ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req)
+ ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
if !ok {
return errorResponse(single(accountId), err)
}
@@ -658,7 +699,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
g.respond(w, r, func(req Request) Response {
allAccountIds := req.AllAccountIds()
- ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req)
+ ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
if !ok {
return errorResponse(allAccountIds, err)
}
@@ -698,12 +739,14 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
// TODO offset and limit over the aggregated results by account
- return etagResponse(allAccountIds, EmailSearchSnippetsResults{
+ body := EmailSearchSnippetsResults{
Results: flattened,
Total: totalOverAllAccounts,
Limit: limit,
QueryState: state,
- }, sessionState, EmailResponseObjectType, state, lang)
+ }
+
+ return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
} else {
withThreads := true
@@ -735,12 +778,14 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
// TODO offset and limit over the aggregated results by account
- return etagResponse(allAccountIds, EmailSearchResults{
+ body := EmailSearchResults{
Results: flattened,
Total: totalAcrossAllAccounts,
Limit: limit,
QueryState: state,
- }, sessionState, EmailResponseObjectType, state, lang)
+ }
+
+ return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
}
})
}
@@ -845,12 +890,15 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
return errorResponse(single(accountId), gwerr)
}
- replaceId := chi.URLParam(r, UriParamEmailId)
+ replaceId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
var body jmap.EmailCreate
- err := req.body(&body)
+ err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -883,10 +931,7 @@ type SwaggerUpdateEmailBody struct {
func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- emailId := chi.URLParam(r, UriParamEmailId)
-
l := req.logger.With()
- l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -894,10 +939,16 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
}
l.Str(logAccountId, accountId)
+ emailId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l.Str(UriParamEmailId, log.SafeString(emailId))
+
logger := log.From(l)
var body map[string]any
- err := req.body(&body)
+ err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -936,10 +987,7 @@ func (e emailKeywordUpdates) IsEmpty() bool {
func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- emailId := chi.URLParam(r, UriParamEmailId)
-
l := req.logger.With()
- l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -947,10 +995,16 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
}
l.Str(logAccountId, accountId)
+ emailId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l.Str(UriParamEmailId, log.SafeString(emailId))
+
logger := log.From(l)
var body emailKeywordUpdates
- err := req.body(&body)
+ err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -1000,10 +1054,7 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
// 500: ErrorResponse500
func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- emailId := chi.URLParam(r, UriParamEmailId)
-
l := req.logger.With()
- l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -1011,10 +1062,16 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
}
l.Str(logAccountId, accountId)
+ emailId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l.Str(UriParamEmailId, log.SafeString(emailId))
+
logger := log.From(l)
var body []string
- err := req.body(&body)
+ err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -1065,21 +1122,24 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
// 500: ErrorResponse500
func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- emailId := chi.URLParam(r, UriParamEmailId)
-
l := req.logger.With()
- l.Str(UriParamEmailId, log.SafeString(emailId))
- accountId, gwerr := req.GetAccountIdForMail()
- if gwerr != nil {
- return errorResponse(single(accountId), gwerr)
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return errorResponse(single(accountId), err)
}
l.Str(logAccountId, accountId)
+ emailId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l.Str(UriParamEmailId, log.SafeString(emailId))
+
logger := log.From(l)
var body []string
- err := req.body(&body)
+ err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -1130,16 +1190,19 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
// 500: ErrorResponse500
func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- emailId := chi.URLParam(r, UriParamEmailId)
-
l := req.logger.With()
- l.Str(UriParamEmailId, emailId)
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return errorResponse(single(accountId), gwerr)
}
- l.Str(logAccountId, accountId)
+ l.Str(logAccountId, log.SafeString(accountId))
+
+ emailId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
@@ -1188,6 +1251,7 @@ type SwaggerDeleteEmailsBody struct {
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
+ /// @api body
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
@@ -1237,7 +1301,10 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) {
}
l.Str(logAccountId, accountId)
- emailId := chi.URLParam(r, UriParamEmailId)
+ emailId, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
l.Str(UriParamEmailId, log.SafeString(emailId))
identityId, err := req.getMandatoryStringParam(QueryParamIdentityId)
@@ -1333,10 +1400,8 @@ func relatedEmailsFilter(email jmap.Email, beacon time.Time, days uint) jmap.Ema
}
func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, UriParamEmailId)
-
g.respond(w, r, func(req Request) Response {
- l := req.logger.With().Str(logEmailId, log.SafeString(id))
+ l := req.logger.With()
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -1344,6 +1409,12 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, log.SafeString(accountId))
+ id, err := req.PathParam(UriParamEmailId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l = l.Str(logEmailId, log.SafeString(id))
+
limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit
if err != nil {
return errorResponse(single(accountId), err)
@@ -1715,7 +1786,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
return errorResponse(allAccountIds, err)
}
if offset > 0 {
- return notImplementesResponse()
+ return notImplementedResponse()
}
if ok {
l = l.Uint(QueryParamOffset, limit)
diff --git a/services/groupware/pkg/groupware/groupware_api_identity.go b/services/groupware/pkg/groupware/groupware_api_identity.go
index 74d884489b..65c56e738b 100644
--- a/services/groupware/pkg/groupware/groupware_api_identity.go
+++ b/services/groupware/pkg/groupware/groupware_api_identity.go
@@ -5,7 +5,6 @@ import (
"net/http"
"strings"
- "github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
@@ -48,7 +47,10 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
if err != nil {
return errorResponse(single(accountId), err)
}
- id := chi.URLParam(r, UriParamIdentityId)
+ id, err := req.PathParam(UriParamIdentityId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id))
res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, req.session, req.ctx, logger, req.language(), []string{id})
if jerr != nil {
@@ -57,7 +59,8 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
if len(res) < 1 {
return notFoundResponse(single(accountId), sessionState)
}
- return etagResponse(single(accountId), res[0], sessionState, IdentityResponseObjectType, state, lang)
+ var body jmap.Identity = res[0]
+ return etagResponse(single(accountId), body, sessionState, IdentityResponseObjectType, state, lang)
})
}
@@ -105,6 +108,7 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
})
}
+// Delete an identity.
func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
@@ -113,10 +117,13 @@ func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
- id := chi.URLParam(r, UriParamIdentityId)
+ id, err := req.PathParam(UriParamIdentityId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
ids := strings.Split(id, ",")
if len(ids) < 1 {
- return req.parameterErrorResponse(single(accountId), UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
+ return req.parameterErrorResponse(single(accountId), UriParamIdentityId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
}
deletion, sessionState, state, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids)
diff --git a/services/groupware/pkg/groupware/groupware_api_index.go b/services/groupware/pkg/groupware/groupware_api_index.go
index 9d2e975479..f4c6bf225d 100644
--- a/services/groupware/pkg/groupware/groupware_api_index.go
+++ b/services/groupware/pkg/groupware/groupware_api_index.go
@@ -153,6 +153,7 @@ type SwaggerIndexResponse struct {
// swagger:route GET /groupware bootstrap index
// Get initial bootstrapping information for a user.
+// @api:tag bootstrap
//
// responses:
//
@@ -166,13 +167,14 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
return req.errorResponseFromJmap(accountIds, err)
}
- return etagResponse(accountIds, IndexResponse{
+ var RBODY IndexResponse = IndexResponse{
Version: Version,
Capabilities: Capabilities,
Limits: buildIndexLimits(req.session),
Accounts: buildIndexAccounts(req.session, boot),
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
- }, sessionState, IndexResponseObjectType, state, lang)
+ }
+ return etagResponse(accountIds, RBODY, sessionState, IndexResponseObjectType, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_mailbox.go b/services/groupware/pkg/groupware/groupware_api_mailbox.go
index f918681b26..a9f5cb8a2a 100644
--- a/services/groupware/pkg/groupware/groupware_api_mailbox.go
+++ b/services/groupware/pkg/groupware/groupware_api_mailbox.go
@@ -5,7 +5,6 @@ import (
"slices"
"strings"
- "github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"github.com/opencloud-eu/opencloud/pkg/jmap"
@@ -35,13 +34,17 @@ type SwaggerGetMailboxById200 struct {
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
- mailboxId := chi.URLParam(r, UriParamMailboxId)
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
+ mailboxId, err := req.PathParam(UriParamMailboxId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+
mailboxes, sessionState, state, lang, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, req.language(), []string{mailboxId})
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
@@ -92,22 +95,21 @@ type SwaggerMailboxesResponse200 struct {
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
- q := r.URL.Query()
- var filter jmap.MailboxFilterCondition
-
- hasCriteria := false
- name := q.Get(QueryParamMailboxSearchName)
- if name != "" {
- filter.Name = name
- hasCriteria = true
- }
- role := q.Get(QueryParamMailboxSearchRole)
- if role != "" {
- filter.Role = role
- hasCriteria = true
- }
-
g.respond(w, r, func(req Request) Response {
+ var filter jmap.MailboxFilterCondition
+
+ hasCriteria := false
+ name, ok := req.getStringParam(QueryParamMailboxSearchName, "") // the mailbox name to filter on
+ if ok && name != "" {
+ filter.Name = name
+ hasCriteria = true
+ }
+ role, ok := req.getStringParam(QueryParamMailboxSearchRole, "") // the mailbox role to filter on
+ if role != "" {
+ filter.Role = role
+ hasCriteria = true
+ }
+
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
@@ -157,8 +159,7 @@ type SwaggerMailboxesForAllAccountsResponse200 struct {
}
// swagger:route GET /groupware/accounts/all/mailboxes mailboxesforallaccounts mailbox
-// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the
-// role of the mailboxes.
+// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the role of the mailboxes.
//
// responses:
//
@@ -166,28 +167,24 @@ type SwaggerMailboxesForAllAccountsResponse200 struct {
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Request) {
- q := r.URL.Query()
- var filter jmap.MailboxFilterCondition
-
- hasCriteria := false
- role := q.Get(QueryParamMailboxSearchRole)
- if role != "" {
- filter.Role = role
- hasCriteria = true
- }
-
g.respond(w, r, func(req Request) Response {
accountIds := req.AllAccountIds()
if len(accountIds) < 1 {
- return noContentResponse(nil, "")
+ return noContentResponse(nil, "") // when the user has no accounts
}
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
- subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false)
- if err != nil {
- return errorResponse(accountIds, err)
+ var filter jmap.MailboxFilterCondition
+ hasCriteria := false
+
+ if role, set := req.getStringParam(QueryParamMailboxSearchRole, ""); set {
+ filter.Role = role
+ hasCriteria = true
}
- if set {
+
+ if subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false); err != nil {
+ return errorResponse(accountIds, err)
+ } else if set {
filter.IsSubscribed = &subscribed
hasCriteria = true
}
@@ -208,22 +205,28 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re
})
}
+// Retrieve Mailboxes by their role for all accounts.
func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *http.Request) {
- role := chi.URLParam(r, UriParamRole)
g.respond(w, r, func(req Request) Response {
accountIds := req.AllAccountIds()
if len(accountIds) < 1 {
- return noContentResponse(nil, "")
+ return noContentResponse(nil, "") // when the user has no accounts
}
+
+ role, err := req.PathParamDoc(UriParamRole, "Role of the mailboxes to retrieve across all accounts")
+ if err != nil {
+ return errorResponse(accountIds, err)
+ }
+
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)).Str("role", role))
filter := jmap.MailboxFilterCondition{
Role: role,
}
- mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
- if err != nil {
- return req.errorResponseFromJmap(accountIds, err)
+ mailboxesByAccountId, sessionState, state, lang, jerr := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
+ if jerr != nil {
+ return req.errorResponseFromJmap(accountIds, jerr)
}
return etagResponse(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
})
@@ -245,11 +248,8 @@ type SwaggerMailboxChangesResponse200 struct {
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
- mailboxId := chi.URLParam(r, UriParamMailboxId)
- sinceState := r.Header.Get(HeaderSince)
-
g.respond(w, r, func(req Request) Response {
- l := req.logger.With().Str(HeaderSince, sinceState)
+ l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
@@ -257,6 +257,11 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, accountId)
+ mailboxId, err := req.PathParam(UriParamMailboxId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return errorResponse(single(accountId), err)
@@ -265,6 +270,12 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
+ sinceState, err := req.HeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list mailbox changes")
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l = l.Str(HeaderParamSince, log.SafeString(sinceState))
+
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, sinceState, true, g.config.maxBodyValueBytes, maxChanges)
@@ -329,6 +340,8 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
})
}
+// Retrieve the roles of all the Mailboxes of all Accounts.
+// @api:example mailboxrolesbyaccount
func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
@@ -346,10 +359,8 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
- mailboxId := chi.URLParam(r, UriParamMailboxId)
-
g.respond(w, r, func(req Request) Response {
- l := req.logger.With().Str(UriParamMailboxId, log.SafeString(mailboxId))
+ l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
@@ -357,6 +368,12 @@ func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, accountId)
+ mailboxId, err := req.PathParamDoc(UriParamMailboxId, "the identifier of the mailbox to update")
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l = l.Str(UriParamMailboxId, log.SafeString(mailboxId))
+
var body jmap.MailboxChange
err = req.body(&body)
if err != nil {
@@ -398,10 +415,12 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
})
}
+// Delete Mailboxes by their unique identifiers.
+//
+// Returns the identifiers of the Mailboxes that have successfully been deleted.
+//
+// @api:example deletedmailboxes
func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
- mailboxId := chi.URLParam(r, UriParamMailboxId)
- mailboxIds := strings.Split(mailboxId, ",")
-
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
@@ -410,11 +429,16 @@ func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, accountId)
+ mailboxIds, err := req.PathListParamDoc(UriParamMailboxId, "the identifier of the mailbox to delete")
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
+ l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
+
if len(mailboxIds) < 1 {
- return noContentResponse(single(accountId), req.session.State)
+ return noContentResponse(single(accountId), req.session.State) // no mailbox identifiers were mentioned in the request
}
- l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
logger := log.From(l)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, req.session, req.ctx, logger, req.language(), "", mailboxIds)
@@ -441,8 +465,8 @@ func scoreMailbox(m jmap.Mailbox) int {
return 1000
}
-func sortMailboxesMap[K comparable](mailboxesByAccountId map[K][]jmap.Mailbox) map[K][]jmap.Mailbox {
- sortedByAccountId := make(map[K][]jmap.Mailbox, len(mailboxesByAccountId))
+func sortMailboxesMap(mailboxesByAccountId map[string][]jmap.Mailbox) map[string][]jmap.Mailbox {
+ sortedByAccountId := make(map[string][]jmap.Mailbox, len(mailboxesByAccountId))
for accountId, unsorted := range mailboxesByAccountId {
mailboxes := make([]jmap.Mailbox, len(unsorted))
copy(mailboxes, unsorted)
diff --git a/services/groupware/pkg/groupware/groupware_api_quota.go b/services/groupware/pkg/groupware/groupware_api_quota.go
index a57072bce3..2237ada3f6 100644
--- a/services/groupware/pkg/groupware/groupware_api_quota.go
+++ b/services/groupware/pkg/groupware/groupware_api_quota.go
@@ -17,6 +17,10 @@ type SwaggerGetQuotaResponse200 struct {
// swagger:route GET /groupware/accounts/{account}/quota quota get_quota
// Get quota limits.
//
+// Retrieves the list of Quota configurations for a given account.
+//
+// Note that there may be multiple Quota objects for different resource types.
+//
// responses:
//
// 200: GetQuotaResponse200
@@ -35,7 +39,8 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) {
return req.errorResponseFromJmap(single(accountId), jerr)
}
for _, v := range res {
- return etagResponse(single(accountId), v.List, sessionState, QuotaResponseObjectType, state, lang)
+ body := v.List
+ return etagResponse(single(accountId), body, sessionState, QuotaResponseObjectType, state, lang)
}
return notFoundResponse(single(accountId), sessionState)
})
@@ -56,6 +61,9 @@ type SwaggerGetQuotaForAllAccountsResponse200 struct {
// swagger:route GET /groupware/accounts/all/quota quota get_quota_for_all_accounts
// Get quota limits for all accounts.
//
+// Retrieves the Quota configuration for all the accounts the user currently has access to,
+// as a dictionary that has the account identifier as its key and an array of Quotas as its value.
+//
// responses:
//
// 200: GetQuotaForAllAccountsResponse200
@@ -65,7 +73,7 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
g.respond(w, r, func(req Request) Response {
accountIds := req.AllAccountIds()
if len(accountIds) < 1 {
- return noContentResponse(accountIds, "")
+ return noContentResponse(accountIds, "") // user has no accounts
}
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
diff --git a/services/groupware/pkg/groupware/groupware_api_tasklists.go b/services/groupware/pkg/groupware/groupware_api_tasklists.go
index 221d6184a1..931b7a3c6a 100644
--- a/services/groupware/pkg/groupware/groupware_api_tasklists.go
+++ b/services/groupware/pkg/groupware/groupware_api_tasklists.go
@@ -3,7 +3,6 @@ package groupware
import (
"net/http"
- "github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
)
@@ -31,7 +30,8 @@ func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) {
}
var _ string = accountId
- return etagResponse(single(accountId), AllTaskLists, req.session.State, TaskListResponseObjectType, TaskListsState, "")
+ var body []jmap.TaskList = AllTaskLists
+ return etagResponse(single(accountId), body, req.session.State, TaskListResponseObjectType, TaskListsState, "")
})
}
@@ -61,7 +61,10 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
}
var _ string = accountId
- tasklistId := chi.URLParam(r, UriParamTaskListId)
+ tasklistId, err := req.PathParam(UriParamTaskListId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
// TODO replace with proper implementation
for _, tasklist := range AllTaskLists {
if tasklist.Id == tasklistId {
@@ -96,7 +99,10 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
}
var _ string = accountId
- tasklistId := chi.URLParam(r, UriParamTaskListId)
+ tasklistId, err := req.PathParam(UriParamTaskListId)
+ if err != nil {
+ return errorResponse(single(accountId), err)
+ }
// TODO replace with proper implementation
tasks, ok := TaskMapByTaskListId[tasklistId]
if !ok {
diff --git a/services/groupware/pkg/groupware/groupware_error.go b/services/groupware/pkg/groupware/groupware_error.go
index 1c1d177abd..b97a5a051a 100644
--- a/services/groupware/pkg/groupware/groupware_error.go
+++ b/services/groupware/pkg/groupware/groupware_error.go
@@ -194,7 +194,7 @@ const (
ErrorCodeMissingContactsSessionCapability = "MSCCON"
ErrorCodeMissingContactsAccountCapability = "MACCON"
ErrorCodeMissingTasksSessionCapability = "MSCTSK"
- ErrorCodeMissingTaskAccountCapability = "MACTSK"
+ ErrorCodeMissingTasksAccountCapability = "MACTSK"
ErrorCodeFailedToDeleteEmail = "DELEML"
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
ErrorCodeFailedToSanitizeEmail = "FSANEM"
@@ -318,12 +318,6 @@ var (
Title: "Invalid Request",
Detail: "The request is invalid.",
}
- ErrorIndeterminateAccount = GroupwareError{
- Status: http.StatusBadRequest,
- Code: ErrorCodeNonExistingAccount,
- Title: "Invalid Account Parameter",
- Detail: "The account the request is for does not exist.",
- }
ErrorNonExistingAccount = GroupwareError{
Status: http.StatusBadRequest,
Code: ErrorCodeIndeterminateAccount,
@@ -392,39 +386,39 @@ var (
}
ErrorMissingCalendarsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
- Code: ErrorCodeMissingCalendarsSessionCapability,
- Title: "Session is missing the task capability '" + jmap.JmapCalendars + "'",
- Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ Code: ErrorCodeMissingCalendarsAccountCapability,
+ Title: "Session is missing the calendars session capability",
+ Detail: "The JMAP Session of the user does not have the required capability for calendars.",
}
ErrorMissingCalendarsAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingCalendarsSessionCapability,
- Title: "Account is missing the task capability '" + jmap.JmapCalendars + "'",
- Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ Title: "Account is missing the calendars capability",
+ Detail: "The JMAP Account of the user does not have the required capability for calendars.",
}
ErrorMissingContactsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
- Code: ErrorCodeMissingContactsSessionCapability,
- Title: "Session is missing the task capability '" + jmap.JmapContacts + "'",
- Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapContacts + "'.",
+ Code: ErrorCodeMissingContactsAccountCapability,
+ Title: "Session is missing the contacts capability",
+ Detail: "The JMAP Session of the user does not have the required capability for accounts.",
}
ErrorMissingContactsAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
- Code: ErrorCodeMissingContactsSessionCapability,
- Title: "Account is missing the task capability '" + jmap.JmapContacts + "'",
- Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapContacts + "'.",
+ Code: ErrorCodeMissingContactsAccountCapability,
+ Title: "Account is missing the contacts capability",
+ Detail: "The JMAP Account of the user does not have the required capability for accounts.",
}
ErrorMissingTasksSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingTasksSessionCapability,
- Title: "Session is missing the task capability '" + jmap.JmapTasks + "'",
- Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ Title: "Session is missing the tasks capability",
+ Detail: "The JMAP Session of the user does not have the required capability for tasks.",
}
ErrorMissingTasksAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
- Code: ErrorCodeMissingTasksSessionCapability,
- Title: "Account is missing the task capability '" + jmap.JmapTasks + "'",
- Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ Code: ErrorCodeMissingTasksAccountCapability,
+ Title: "Account is missing the tasks capability",
+ Detail: "The JMAP Account of the user does not have the required capability for tasks",
}
ErrorFailedToDeleteEmail = GroupwareError{
Status: http.StatusInternalServerError,
diff --git a/services/groupware/pkg/groupware/groupware_examples_test.go b/services/groupware/pkg/groupware/groupware_examples_test.go
new file mode 100644
index 0000000000..f1b3cc3313
--- /dev/null
+++ b/services/groupware/pkg/groupware/groupware_examples_test.go
@@ -0,0 +1,219 @@
+//go:build groupware_examples
+
+package groupware
+
+import (
+ "time"
+
+ "github.com/opencloud-eu/opencloud/pkg/jmap"
+ "github.com/opencloud-eu/opencloud/pkg/structs"
+)
+
+var (
+ exampleQuotaState = "veiv8iez"
+)
+
+type Exampler struct{}
+
+var j = jmap.ExamplerInstance
+
+func Example() {
+ jmap.SerializeExamples(Exampler{})
+ //Output:
+}
+
+func (e Exampler) AccountQuota() AccountQuota {
+ return AccountQuota{
+ Quotas: []jmap.Quota{j.Quota()},
+ State: jmap.State(exampleQuotaState),
+ }
+}
+
+func (e Exampler) AccountQuotaMap() map[string]AccountQuota {
+ return map[string]AccountQuota{
+ j.AccountId: e.AccountQuota(),
+ }
+}
+
+func (e Exampler) AccountWithId() AccountWithId {
+ a, _ := j.Account()
+ return AccountWithId{
+ AccountId: j.AccountId,
+ Account: a,
+ }
+}
+
+func (e Exampler) AccountWithIdAndIdentities() AccountWithIdAndIdentities {
+ a, _ := j.Account()
+ return AccountWithIdAndIdentities{
+ AccountId: j.AccountId,
+ Account: a,
+ Identities: j.Identities(),
+ }
+}
+
+func (e Exampler) IndexAccountMailCapabilities() IndexAccountMailCapabilities {
+ m := j.SessionMailAccountCapabilities()
+ s := j.SessionSubmissionAccountCapabilities()
+ return IndexAccountMailCapabilities{
+ MaxMailboxDepth: m.MaxMailboxDepth,
+ MaxSizeMailboxName: m.MaxSizeMailboxName,
+ MaxMailboxesPerEmail: m.MaxMailboxesPerEmail,
+ MaxSizeAttachmentsPerEmail: m.MaxSizeAttachmentsPerEmail,
+ MayCreateTopLevelMailbox: m.MayCreateTopLevelMailbox,
+ MaxDelayedSend: s.MaxDelayedSend,
+ }
+}
+
+func (e Exampler) IndexAccountSieveCapabilities() IndexAccountSieveCapabilities {
+ s := j.SessionSieveAccountCapabilities()
+ return IndexAccountSieveCapabilities{
+ MaxSizeScriptName: s.MaxSizeScriptName,
+ MaxSizeScript: s.MaxSizeScript,
+ MaxNumberScripts: s.MaxNumberScripts,
+ MaxNumberRedirects: s.MaxNumberRedirects,
+ }
+}
+
+func (e Exampler) IndexAccountCapabilities() IndexAccountCapabilities {
+ return IndexAccountCapabilities{
+ Mail: e.IndexAccountMailCapabilities(),
+ Sieve: e.IndexAccountSieveCapabilities(),
+ }
+}
+
+func (e Exampler) IndexAccount() IndexAccount {
+ a, _ := j.Account()
+ return IndexAccount{
+ AccountId: j.AccountId,
+ Name: a.Name,
+ IsPersonal: a.IsPersonal,
+ IsReadOnly: a.IsReadOnly,
+ Capabilities: e.IndexAccountCapabilities(),
+ Identities: j.Identities(),
+ Quotas: j.Quotas(),
+ }
+}
+
+func (e Exampler) IndexAccounts() []IndexAccount {
+ return []IndexAccount{
+ e.IndexAccount(),
+ {
+ AccountId: j.SharedAccountId,
+ Name: j.SharedAccountName,
+ IsPersonal: false,
+ IsReadOnly: true,
+ Capabilities: e.IndexAccountCapabilities(),
+ Identities: []jmap.Identity{
+ {
+ Id: j.SharedIdentityId,
+ Name: j.SharedIdentityName,
+ Email: j.SharedIdentityEmailAddress,
+ },
+ },
+ Quotas: j.Quotas(),
+ },
+ }
+}
+
+func (e Exampler) IndexPrimaryAccounts() IndexPrimaryAccounts {
+ return IndexPrimaryAccounts{
+ Mail: j.AccountId,
+ Submission: j.AccountId,
+ Blob: j.AccountId,
+ VacationResponse: j.AccountId,
+ Sieve: j.AccountId,
+ }
+}
+
+func (e Exampler) IndexResponse() IndexResponse {
+ return IndexResponse{
+ Version: "4.0.0",
+ Capabilities: []string{"mail:1"},
+ Limits: IndexLimits{
+ MaxSizeUpload: 50000000,
+ MaxConcurrentUpload: 4,
+ MaxSizeRequest: 10000000,
+ MaxConcurrentRequests: 4,
+ },
+ PrimaryAccounts: e.IndexPrimaryAccounts(),
+ Accounts: []IndexAccount{e.IndexAccount()},
+ }
+}
+
+func (e Exampler) ErrorResponse() ErrorResponse {
+ err := apiError("6d9c65d1-0368-4833-b09f-885aa0171b95", ErrorNoMailboxWithDraftRole)
+ return ErrorResponse{
+ Errors: []Error{
+ *err,
+ },
+ }
+}
+
+func (e Exampler) MailboxesByAccountId() (map[string][]jmap.Mailbox, string) {
+ j := jmap.ExamplerInstance
+ return map[string][]jmap.Mailbox{
+ j.AccountId: j.Mailboxes(),
+ }, "All mailboxes for all accounts, without a role filter"
+}
+
+func (e Exampler) MailboxesByAccountIdFilteredOnInboxRole() (map[string][]jmap.Mailbox, string, string) {
+ j := jmap.ExamplerInstance
+ return map[string][]jmap.Mailbox{
+ j.AccountId: structs.Filter(j.Mailboxes(), func(m jmap.Mailbox) bool { return m.Role == jmap.JmapMailboxRoleInbox }),
+ }, "All mailboxes for all accounts, filtered on the 'inbox' role", "inboxrole"
+}
+
+func (e Exampler) EmailSearchResults() EmailSearchResults {
+ sent, _ := time.Parse(time.RFC3339, "2026-01-12T21:46:01Z")
+ received, _ := time.Parse(time.RFC3339, "2026-01-12T21:47:21Z")
+ j := jmap.ExamplerInstance
+ return EmailSearchResults{
+ Results: []jmap.Email{
+ {
+ Id: "ov7ienge",
+ BlobId: "ccyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasiba",
+ ThreadId: "is",
+ MailboxIds: map[string]bool{j.MailboxInboxId: true},
+ Keywords: map[string]bool{jmap.JmapKeywordAnswered: true},
+ Size: 1084,
+ ReceivedAt: received,
+ MessageId: []string{"1768845021.1753110@example.com"},
+ Sender: []jmap.EmailAddress{
+ {Name: j.SenderName, Email: j.SenderEmailAddress},
+ },
+ From: []jmap.EmailAddress{
+ {Name: j.SenderName, Email: j.SenderEmailAddress},
+ },
+ To: []jmap.EmailAddress{
+ {Name: j.IdentityName, Email: j.EmailAddress},
+ },
+ Subject: "Remember the Cant",
+ SentAt: sent,
+ TextBody: []jmap.EmailBodyPart{
+ {PartId: "1", BlobId: "ckyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasibnebdw", Size: 115, Type: "text/plain", Charset: "utf-8"},
+ },
+ HtmlBody: []jmap.EmailBodyPart{
+ {PartId: "2", BlobId: "ckyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasibnsbvjae", Size: 163, Type: "text/html", Charset: "utf-8"},
+ },
+ Preview: "The Canterbury was destroyed while investigating a false distress call from the Scopuli.",
+ },
+ },
+ Total: 132,
+ Limit: 1,
+ QueryState: "seehug3p",
+ }
+}
+
+func (e Exampler) MailboxRolesByAccounts() (map[string][]string, string, string, string) {
+ j := jmap.ExamplerInstance
+ return map[string][]string{
+ j.AccountId: jmap.JmapMailboxRoles,
+ j.SharedAccountId: jmap.JmapMailboxRoles,
+ }, "Roles of the Mailboxes of each Account", "", "mailboxrolesbyaccount"
+}
+
+func (e Exampler) DeletedMailboxes() ([]string, string, string, string) {
+ j := jmap.ExamplerInstance
+ return []string{j.MailboxProjectId, j.MailboxJunkId}, "Identifiers of the Mailboxes that have successfully been deleted", "", "deletedmailboxes"
+}
diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go
index beb5d8b4ec..29dff5ff84 100644
--- a/services/groupware/pkg/groupware/groupware_request.go
+++ b/services/groupware/pkg/groupware/groupware_request.go
@@ -68,14 +68,64 @@ var (
errNoPrimaryAccountForBlob = errors.New("no primary account for blob")
errNoPrimaryAccountForVacationResponse = errors.New("no primary account for vacation response")
errNoPrimaryAccountForSubmission = errors.New("no primary account for submission")
- errNoPrimaryAccountForTask = errors.New("no primary account for task")
- errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
- errNoPrimaryAccountForContact = errors.New("no primary account for contact")
errNoPrimaryAccountForQuota = errors.New("no primary account for quota")
+ // errNoPrimaryAccountForTask = errors.New("no primary account for task")
+ // errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
+ // errNoPrimaryAccountForContact = errors.New("no primary account for contact")
// errNoPrimaryAccountForSieve = errors.New("no primary account for sieve")
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
)
+func (r Request) HeaderParam(name string) (string, *Error) {
+ value := r.r.Header.Get(name)
+ if value == "" {
+ msg := fmt.Sprintf("Missing mandatory request header '%s'", name)
+ return "", r.observedParameterError(ErrorInvalidRequestParameter,
+ withDetail(msg),
+ withSource(&ErrorSource{Header: name}),
+ )
+ } else {
+ return value, nil
+ }
+}
+
+func (r Request) HeaderParamDoc(name string, _ string) (string, *Error) {
+ return r.HeaderParam(name)
+}
+
+func (r Request) OptHeaderParam(name string) string {
+ return r.r.Header.Get(name)
+}
+
+func (r Request) OptHeaderParamDoc(name string, _ string) string {
+ return r.OptHeaderParam(name)
+}
+
+func (r Request) PathParam(name string) (string, *Error) {
+ value := chi.URLParam(r.r, name)
+ if value == "" {
+ msg := fmt.Sprintf("Missing mandatory path parameter '%s'", name)
+ return "", r.observedParameterError(ErrorInvalidRequestParameter,
+ withDetail(msg),
+ withSource(&ErrorSource{Parameter: name}),
+ )
+ } else {
+ return value, nil
+ }
+}
+
+func (r Request) PathParamDoc(name string, _ string) (string, *Error) {
+ return r.PathParam(name)
+}
+
+func (r Request) PathListParamDoc(name string, _ string) ([]string, *Error) {
+ value, err := r.PathParam(name)
+ if err != nil {
+ return nil, err
+ }
+ return strings.Split(value, ","), nil
+}
+
func (r Request) AllAccountIds() []string {
// TODO potentially filter on "subscribed" accounts?
return structs.Uniq(structs.Keys(r.session.Accounts))
@@ -313,6 +363,26 @@ func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
return result, true, nil
}
+func (r Request) parseOptStringListParam(param string) ([]string, bool, *Error) {
+ result := []string{}
+ q := r.r.URL.Query()
+ if !q.Has(param) {
+ return nil, false, nil
+ }
+ for _, value := range q[param] {
+ for _, v := range strings.Split(value, ",") {
+ if strings.TrimSpace(v) != "" {
+ result = append(result, v)
+ }
+ }
+ }
+ return result, true, nil
+}
+
+func (r Request) bodydoc(target any, _ string) *Error {
+ return r.body(target)
+}
+
func (r Request) body(target any) *Error {
body := r.r.Body
defer func(b io.ReadCloser) {
diff --git a/services/groupware/pkg/groupware/groupware_response.go b/services/groupware/pkg/groupware/groupware_response.go
index b82fb63d82..74366bf89b 100644
--- a/services/groupware/pkg/groupware/groupware_response.go
+++ b/services/groupware/pkg/groupware/groupware_response.go
@@ -163,7 +163,7 @@ func etagNotFoundResponse(accountIds []string, sessionState jmap.SessionState, o
}
}
-func notImplementesResponse() Response {
+func notImplementedResponse() Response {
return Response{
body: nil,
status: http.StatusNotImplemented,
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index 9dcf755851..08f21bc0d1 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -11,19 +11,20 @@ var (
)
const (
- UriParamAccountId = "accountid"
- UriParamMailboxId = "mailboxid"
- UriParamEmailId = "emailid"
- UriParamIdentityId = "identityid"
- UriParamBlobId = "blobid"
+ UriParamAccountId = "accountid" // Identifier of the account
+ UriParamMailboxId = "mailboxid" // Identifier of the mailbox
+ UriParamEmailId = "emailid" // Identifier of the email
+ UriParamIdentityId = "identityid" // Identifier of the identity
+ UriParamBlobId = "blobid" // Identifier of theblob
+ UriParamStreamId = "stream" // Identifier of the stream
+ UriParamAddressBookId = "addressbookid" // Identifier of the address book
+ UriParamCalendarId = "calendarid" // Identifier of the calendar
+ UriParamTaskListId = "tasklistid" // Identifier of the tasklist
+ UriParamContactId = "contactid" // Identifier of the contact
+ UriParamEventId = "eventid" // Idenfitier of the event
UriParamBlobName = "blobname"
- UriParamStreamId = "stream"
+ UriParamSince = "since"
UriParamRole = "role"
- UriParamAddressBookId = "addressbookid"
- UriParamCalendarId = "calendarid"
- UriParamTaskListId = "tasklistid"
- UriParamContactId = "contactid"
- UriParamEventId = "eventid"
QueryParamMailboxSearchName = "name"
QueryParamMailboxSearchRole = "role"
QueryParamMailboxSearchSubscribed = "subscribed"
@@ -57,7 +58,7 @@ const (
QueryParamSeen = "seen"
QueryParamUndesirable = "undesirable"
QueryParamMarkAsSeen = "markAsSeen"
- HeaderSince = "if-none-match"
+ HeaderParamSince = "if-none-match"
)
func (g *Groupware) Route(r chi.Router) {
@@ -100,6 +101,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Route("/{mailboxid}", func(r chi.Router) {
r.Get("/", g.GetMailbox)
r.Get("/emails", g.GetAllEmailsInMailbox)
+ r.Get("/emails/since/{since}", g.GetAllEmailsInMailboxSince)
r.Get("/changes", g.GetMailboxChanges)
r.Patch("/", g.UpdateMailbox)
r.Delete("/", g.DeleteMailbox)
diff --git a/services/groupware/pkg/middleware/auth.go b/services/groupware/pkg/middleware/auth.go
index 6c8b76fb82..2f85e62074 100644
--- a/services/groupware/pkg/middleware/auth.go
+++ b/services/groupware/pkg/middleware/auth.go
@@ -29,7 +29,7 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
opt := authOptions(opts...)
tokenManager, err := jwt.New(map[string]any{
"secret": opt.JWTSecret,
- "expires": int64(24 * 60 * 60),
+ "expires": int64(24 * 60 * 60), // token expiration in seconds
})
if err != nil {
opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
@@ -39,19 +39,37 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
ctx := r.Context()
t := r.Header.Get(revactx.TokenHeader)
if t == "" {
- opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Msgf("missing access token in header %v", revactx.TokenHeader)
+ requestID := r.Header.Get("X-Request-ID")
+ traceID := GetTraceID(ctx)
+ l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
+ if traceID != "" {
+ l = l.Str(LogTraceID, log.SafeString(traceID))
+ }
+ l.Msgf("missing access token in header %v", revactx.TokenHeader)
w.WriteHeader(http.StatusUnauthorized) // missing access token
return
}
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t)
if err != nil {
- opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader)
+ requestID := r.Header.Get("X-Request-ID")
+ traceID := GetTraceID(ctx)
+ l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
+ if traceID != "" {
+ l = l.Str(LogTraceID, log.SafeString(traceID))
+ }
+ l.Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader)
w.WriteHeader(http.StatusUnauthorized) // invalid token
return
}
if ok, err := scope.VerifyScope(ctx, tokenScope, r); err != nil || !ok {
- opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msg("verifying scope failed")
+ requestID := r.Header.Get("X-Request-ID")
+ traceID := GetTraceID(ctx)
+ l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
+ if traceID != "" {
+ l = l.Str(LogTraceID, log.SafeString(traceID))
+ }
+ l.Err(err).Msg("verifying scope failed")
w.WriteHeader(http.StatusUnauthorized) // invalid scope
return
}
diff --git a/services/groupware/pkg/middleware/groupware_logger.go b/services/groupware/pkg/middleware/groupware_logger.go
index bdea8fe7a6..ff4bb2b187 100644
--- a/services/groupware/pkg/middleware/groupware_logger.go
+++ b/services/groupware/pkg/middleware/groupware_logger.go
@@ -8,6 +8,10 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
)
+var (
+ LogTraceID = "traceId"
+)
+
func GroupwareLogger(logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -37,12 +41,10 @@ func GroupwareLogger(logger log.Logger) func(http.Handler) http.Handler {
ctx := r.Context()
requestID := middleware.GetReqID(ctx)
+ level = level.Str(log.RequestIDString, log.SafeString(requestID))
traceID := GetTraceID(ctx)
-
- level.Str(log.RequestIDString, requestID)
-
if traceID != "" {
- level.Str("traceId", traceID)
+ level = level.Str(LogTraceID, log.SafeString(traceID))
}
level.
diff --git a/services/groupware/pnpm-lock.yaml b/services/groupware/pnpm-lock.yaml
index 7ecf81fb8b..0093c7de51 100644
--- a/services/groupware/pnpm-lock.yaml
+++ b/services/groupware/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@redocly/cli':
- specifier: ^2.4.0
- version: 2.4.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)
+ specifier: ^2.14.5
+ version: 2.14.5(@opentelemetry/api@1.9.0)(core-js@3.45.1)
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
@@ -18,45 +18,45 @@ importers:
specifier: ^1.1.2
version: 1.1.2
js-yaml:
- specifier: ^4.1.0
- version: 4.1.0
+ specifier: ^4.1.1
+ version: 4.1.1
ts-node:
specifier: ^10.9.2
- version: 10.9.2(@types/node@24.3.1)(typescript@5.9.2)
+ version: 10.9.2(@types/node@24.10.7)(typescript@5.9.3)
typescript:
- specifier: ^5.9.2
- version: 5.9.2
+ specifier: ^5.9.3
+ version: 5.9.3
devDependencies:
'@types/node':
- specifier: ^24.3.1
- version: 24.3.1
+ specifier: ^24.10.7
+ version: 24.10.7
packages:
- '@babel/code-frame@7.27.1':
- resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ '@babel/code-frame@7.28.6':
+ resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-identifier@7.27.1':
- resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
- '@babel/runtime@7.28.4':
- resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+ '@babel/runtime@7.28.6':
+ resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
- '@emotion/is-prop-valid@1.2.2':
- resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==}
+ '@emotion/is-prop-valid@1.4.0':
+ resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==}
- '@emotion/memoize@0.8.1':
- resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
+ '@emotion/memoize@0.9.0':
+ resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==}
- '@emotion/unitless@0.8.1':
- resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
+ '@emotion/unitless@0.10.0':
+ resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
'@exodus/schemasafe@1.3.0':
resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==}
@@ -81,10 +81,6 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
- '@jest/schemas@29.6.3':
- resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -201,40 +197,34 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
- '@redocly/ajv@8.11.2':
- resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
+ '@redocly/ajv@8.17.1':
+ resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==}
- '@redocly/ajv@8.11.3':
- resolution: {integrity: sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==}
-
- '@redocly/cli@2.4.0':
- resolution: {integrity: sha512-RXINsLA5cFKZM0zB66/1rWWU4oENBZ5lcrnwcyoHQmaGt+rA7Glf/lORc9/JmkznVESFZ3t/9nsc7mSfGfwuAw==}
+ '@redocly/cli@2.14.5':
+ resolution: {integrity: sha512-02Zz7YS7UwfBpbHbF64ApUkspr8Ar2XytgZ7JUljVwz+VjzCRcxkGMGE82BVYYQNKkw/YwlNOIX+lYYNbowTcw==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
hasBin: true
'@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
- '@redocly/config@0.31.0':
- resolution: {integrity: sha512-KPm2v//zj7qdGvClX0YqRNLQ9K7loVJWFEIceNxJIYPXP4hrhNvOLwjmxIkdkai0SdqYqogR2yjM/MjF9/AGdQ==}
+ '@redocly/config@0.41.2':
+ resolution: {integrity: sha512-G6muhdTKcEV2TECBFzuT905p4a27OgUtwBqRVnMx1JebO6i8zlm6bPB2H3fD1Hl+MiUpk7Jx2kwGmLVgpz5nIg==}
- '@redocly/openapi-core@1.34.5':
- resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==}
+ '@redocly/openapi-core@1.34.6':
+ resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
- '@redocly/openapi-core@2.4.0':
- resolution: {integrity: sha512-ft4qzUu7g9vabIkp09uLwQwpWgT5aycrmUENh0WyvYDZwv4eDg+tDSMP1Grt0nPy+GJ/LnIMpaYAPhrIMMhMzg==}
+ '@redocly/openapi-core@2.14.5':
+ resolution: {integrity: sha512-MQQR+RCG0V+jZV6msgKv1CNi/+TZUXmjMAAuTEktaTOYIsQWTCV9GYSD/2n94eMDZwxI4olr05OPzOZo9z0EMg==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
- '@redocly/respect-core@2.4.0':
- resolution: {integrity: sha512-xpuOC/gySVc8Ncuz3ZKXWNxYI9BLgkuKh0YB7l2xXTgcFz84V3PHKDsivTPkP9eKKbCLZjjzmwJCr1eiFSrHjg==}
+ '@redocly/respect-core@2.14.5':
+ resolution: {integrity: sha512-zZKYwBZYfRi4/Iv2V7hq9xOYhpO3+IuzYjk8/V0CZjoHCnoW8jgGGhvoXMn/BfedZS9/3fV9n4SEskIbmCPl8Q==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
- '@sinclair/typebox@0.27.8':
- resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
-
- '@tsconfig/node10@1.0.11':
- resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
+ '@tsconfig/node10@1.0.12':
+ resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
'@tsconfig/node12@1.0.11':
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
@@ -251,11 +241,11 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
- '@types/node@24.3.1':
- resolution: {integrity: sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==}
+ '@types/node@24.10.7':
+ resolution: {integrity: sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==}
- '@types/stylis@4.2.5':
- resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==}
+ '@types/stylis@4.2.7':
+ resolution: {integrity: sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -277,17 +267,14 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
- ajv-formats@2.1.1:
- resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
- ajv@8.17.1:
- resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
-
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -300,10 +287,6 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
- ansi-styles@5.2.0:
- resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
- engines: {node: '>=10'}
-
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
@@ -318,9 +301,6 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
- asynckit@0.4.0:
- resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
-
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -344,10 +324,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- call-bind-apply-helpers@1.0.2:
- resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
- engines: {node: '>= 0.4'}
-
call-me-maybe@1.0.2:
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
@@ -392,10 +368,6 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
- combined-stream@1.0.8:
- resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
- engines: {node: '>= 0.8'}
-
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
@@ -424,8 +396,8 @@ packages:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
- csstype@3.1.3:
- resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -439,14 +411,6 @@ packages:
decko@1.2.0:
resolution: {integrity: sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==}
- delayed-stream@1.0.0:
- resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
- engines: {node: '>=0.4.0'}
-
- diff-sequences@29.6.3:
- resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
@@ -461,8 +425,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
- dompurify@3.2.7:
- resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
+ dompurify@3.3.1:
+ resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -471,10 +435,6 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
- dunder-proto@1.0.1:
- resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
- engines: {node: '>= 0.4'}
-
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -495,22 +455,6 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
- es-define-property@1.0.1:
- resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
- engines: {node: '>= 0.4'}
-
- es-errors@1.3.0:
- resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
- engines: {node: '>= 0.4'}
-
- es-object-atoms@1.1.1:
- resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
- engines: {node: '>= 0.4'}
-
- es-set-tostringtag@2.1.0:
- resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
- engines: {node: '>= 0.4'}
-
es6-promise@3.3.1:
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
@@ -549,43 +493,24 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
- form-data@4.0.4:
- resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
- engines: {node: '>= 6'}
-
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
- function-bind@1.1.2:
- resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- get-intrinsic@1.3.0:
- resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
- engines: {node: '>= 0.4'}
-
- get-proto@1.0.1:
- resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
- engines: {node: '>= 0.4'}
-
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
- glob@11.0.3:
- resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
+ glob@11.1.0:
+ resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
hasBin: true
- gopd@1.2.0:
- resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
- engines: {node: '>= 0.4'}
-
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
@@ -595,18 +520,6 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
- has-symbols@1.1.0:
- resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
- engines: {node: '>= 0.4'}
-
- has-tostringtag@1.0.2:
- resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
- engines: {node: '>= 0.4'}
-
- hasown@2.0.2:
- resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
- engines: {node: '>= 0.4'}
-
htmlparser2@10.0.0:
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
@@ -651,18 +564,6 @@ packages:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
- jest-diff@29.7.0:
- resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
- jest-get-type@29.6.3:
- resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
- jest-matcher-utils@29.7.0:
- resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
@@ -670,8 +571,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-yaml@4.1.0:
- resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
json-pointer@0.6.2:
@@ -703,8 +604,8 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
- lru-cache@11.2.2:
- resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
+ lru-cache@11.2.4:
+ resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
engines: {node: 20 || >=22}
lunr@2.3.9:
@@ -721,20 +622,8 @@ packages:
engines: {node: '>= 12'}
hasBin: true
- math-intrinsics@1.1.0:
- resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
- engines: {node: '>= 0.4'}
-
- mime-db@1.52.0:
- resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
- engines: {node: '>= 0.6'}
-
- mime-types@2.1.35:
- resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
- engines: {node: '>= 0.6'}
-
- minimatch@10.0.3:
- resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
+ minimatch@10.1.1:
+ resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
minimatch@5.1.6:
@@ -831,8 +720,8 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
- openapi-sampler@1.6.1:
- resolution: {integrity: sha512-s1cIatOqrrhSj2tmJ4abFYZQK6l5v+V4toO5q1Pa0DyN8mtyqy2I+Qrj5W9vOELEtybIMQs/TBZGVO/DtTFK8w==}
+ openapi-sampler@1.6.2:
+ resolution: {integrity: sha512-NyKGiFKfSWAZr4srD/5WDhInOWDhfml32h/FKUqLpEwKJt0kG0LGUU0MdyNkKrVGuJnw6DuPWq/sHCwAMpiRxg==}
outdent@0.8.0:
resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==}
@@ -856,8 +745,8 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
- path-scurry@2.0.0:
- resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
+ path-scurry@2.0.1:
+ resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22}
perfect-scrollbar@1.5.6:
@@ -870,6 +759,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
@@ -885,10 +778,6 @@ packages:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
- pretty-format@29.7.0:
- resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
@@ -906,24 +795,21 @@ packages:
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
- react-dom@19.2.0:
- resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
+ react-dom@19.2.3:
+ resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
peerDependencies:
- react: ^19.2.0
+ react: ^19.2.3
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- react-is@18.3.1:
- resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
-
react-tabs@6.1.0:
resolution: {integrity: sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
- react@19.2.0:
- resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
+ react@19.2.3:
+ resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
@@ -969,8 +855,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- set-cookie-parser@2.7.1:
- resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
shallowequal@1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
@@ -1045,15 +931,15 @@ packages:
strnum@1.1.2:
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
- styled-components@6.1.19:
- resolution: {integrity: sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==}
+ styled-components@6.3.5:
+ resolution: {integrity: sha512-f8jAunVw/r41o17+JlWVlMTsyBKyghCdQ84YCKPxgKSMOZJbK3CKPxeIhotz6hlXvHb0w62zG4yyOdGY0kaB3g==}
engines: {node: '>= 16'}
peerDependencies:
react: '>= 16.8.0'
react-dom: '>= 16.8.0'
- stylis@4.3.2:
- resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==}
+ stylis@4.3.6:
+ resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
@@ -1087,11 +973,11 @@ packages:
'@swc/wasm':
optional: true
- tslib@2.6.2:
- resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
- typescript@5.9.2:
- resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -1100,20 +986,21 @@ packages:
engines: {node: '>=0.8.0'}
hasBin: true
- undici-types@7.10.0:
- resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
+ ulid@3.0.2:
+ resolution: {integrity: sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==}
+ hasBin: true
- undici@6.22.0:
- resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==}
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ undici@6.23.0:
+ resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==}
engines: {node: '>=18.17'}
- undici@7.15.0:
- resolution: {integrity: sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==}
+ undici@7.18.2:
+ resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
engines: {node: '>=20.18.1'}
- uri-js-replace@1.0.1:
- resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
-
url-template@2.0.8:
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
@@ -1134,6 +1021,7 @@ packages:
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
@@ -1195,27 +1083,27 @@ packages:
snapshots:
- '@babel/code-frame@7.27.1':
+ '@babel/code-frame@7.28.6':
dependencies:
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/helper-validator-identifier@7.27.1': {}
+ '@babel/helper-validator-identifier@7.28.5': {}
- '@babel/runtime@7.28.4': {}
+ '@babel/runtime@7.28.6': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
- '@emotion/is-prop-valid@1.2.2':
+ '@emotion/is-prop-valid@1.4.0':
dependencies:
- '@emotion/memoize': 0.8.1
+ '@emotion/memoize': 0.9.0
- '@emotion/memoize@0.8.1': {}
+ '@emotion/memoize@0.9.0': {}
- '@emotion/unitless@0.8.1': {}
+ '@emotion/unitless@0.10.0': {}
'@exodus/schemasafe@1.3.0': {}
@@ -1238,10 +1126,6 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
- '@jest/schemas@29.6.3':
- dependencies:
- '@sinclair/typebox': 0.27.8
-
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
@@ -1352,51 +1236,45 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
- '@redocly/ajv@8.11.2':
+ '@redocly/ajv@8.17.1':
dependencies:
fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
- uri-js-replace: 1.0.1
- '@redocly/ajv@8.11.3':
- dependencies:
- fast-deep-equal: 3.1.3
- json-schema-traverse: 1.0.0
- require-from-string: 2.0.2
- uri-js-replace: 1.0.1
-
- '@redocly/cli@2.4.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)':
+ '@redocly/cli@2.14.5(@opentelemetry/api@1.9.0)(core-js@3.45.1)':
dependencies:
'@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
- '@redocly/openapi-core': 2.4.0(ajv@8.17.1)
- '@redocly/respect-core': 2.4.0(ajv@8.17.1)
+ '@redocly/openapi-core': 2.14.5
+ '@redocly/respect-core': 2.14.5
abort-controller: 3.0.0
+ ajv: '@redocly/ajv@8.17.1'
+ ajv-formats: 3.0.1(@redocly/ajv@8.17.1)
chokidar: 3.6.0
colorette: 1.4.0
cookie: 0.7.2
dotenv: 16.4.7
- form-data: 4.0.4
- glob: 11.0.3
+ glob: 11.1.0
handlebars: 4.7.8
https-proxy-agent: 7.0.6
mobx: 6.15.0
pluralize: 8.0.0
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- redoc: 2.5.1(core-js@3.45.1)(mobx@6.15.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0))
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ redoc: 2.5.1(core-js@3.45.1)(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(styled-components@6.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
semver: 7.7.3
- set-cookie-parser: 2.7.1
+ set-cookie-parser: 2.7.2
simple-websocket: 9.1.0
- styled-components: 6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- undici: 6.22.0
+ styled-components: 6.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ ulid: 3.0.2
+ undici: 6.23.0
yargs: 17.0.1
transitivePeerDependencies:
- '@opentelemetry/api'
- - ajv
- bufferutil
- core-js
- encoding
@@ -1406,57 +1284,52 @@ snapshots:
'@redocly/config@0.22.2': {}
- '@redocly/config@0.31.0':
+ '@redocly/config@0.41.2':
dependencies:
json-schema-to-ts: 2.7.2
- '@redocly/openapi-core@1.34.5':
+ '@redocly/openapi-core@1.34.6':
dependencies:
- '@redocly/ajv': 8.11.3
+ '@redocly/ajv': 8.17.1
'@redocly/config': 0.22.2
colorette: 1.4.0
https-proxy-agent: 7.0.6
js-levenshtein: 1.1.6
- js-yaml: 4.1.0
+ js-yaml: 4.1.1
minimatch: 5.1.6
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
- '@redocly/openapi-core@2.4.0(ajv@8.17.1)':
+ '@redocly/openapi-core@2.14.5':
dependencies:
- '@redocly/ajv': 8.11.3
- '@redocly/config': 0.31.0
- ajv-formats: 2.1.1(ajv@8.17.1)
+ '@redocly/ajv': 8.17.1
+ '@redocly/config': 0.41.2
+ ajv: '@redocly/ajv@8.17.1'
+ ajv-formats: 3.0.1(@redocly/ajv@8.17.1)
colorette: 1.4.0
js-levenshtein: 1.1.6
- js-yaml: 4.1.0
- minimatch: 10.0.3
+ js-yaml: 4.1.1
+ picomatch: 4.0.3
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
- transitivePeerDependencies:
- - ajv
- '@redocly/respect-core@2.4.0(ajv@8.17.1)':
+ '@redocly/respect-core@2.14.5':
dependencies:
'@faker-js/faker': 7.6.0
'@noble/hashes': 1.8.0
- '@redocly/ajv': 8.11.2
- '@redocly/openapi-core': 2.4.0(ajv@8.17.1)
- better-ajv-errors: 1.2.0(ajv@8.17.1)
+ '@redocly/ajv': 8.17.1
+ '@redocly/openapi-core': 2.14.5
+ ajv: '@redocly/ajv@8.17.1'
+ better-ajv-errors: 1.2.0(@redocly/ajv@8.17.1)
colorette: 2.0.20
- jest-matcher-utils: 29.7.0
json-pointer: 0.6.2
jsonpath-rfc9535: 1.3.0
- openapi-sampler: 1.6.1
+ openapi-sampler: 1.6.2
outdent: 0.8.0
- transitivePeerDependencies:
- - ajv
- '@sinclair/typebox@0.27.8': {}
-
- '@tsconfig/node10@1.0.11': {}
+ '@tsconfig/node10@1.0.12': {}
'@tsconfig/node12@1.0.11': {}
@@ -1468,11 +1341,11 @@ snapshots:
'@types/json-schema@7.0.15': {}
- '@types/node@24.3.1':
+ '@types/node@24.10.7':
dependencies:
- undici-types: 7.10.0
+ undici-types: 7.16.0
- '@types/stylis@4.2.5': {}
+ '@types/stylis@4.2.7': {}
'@types/trusted-types@2.0.7':
optional: true
@@ -1489,16 +1362,9 @@ snapshots:
agent-base@7.1.4: {}
- ajv-formats@2.1.1(ajv@8.17.1):
+ ajv-formats@3.0.1(@redocly/ajv@8.17.1):
optionalDependencies:
- ajv: 8.17.1
-
- ajv@8.17.1:
- dependencies:
- fast-deep-equal: 3.1.3
- fast-uri: 3.1.0
- json-schema-traverse: 1.0.0
- require-from-string: 2.0.2
+ ajv: '@redocly/ajv@8.17.1'
ansi-regex@5.0.1: {}
@@ -1508,8 +1374,6 @@ snapshots:
dependencies:
color-convert: 2.0.1
- ansi-styles@5.2.0: {}
-
ansi-styles@6.2.3: {}
anymatch@3.1.3:
@@ -1521,15 +1385,13 @@ snapshots:
argparse@2.0.1: {}
- asynckit@0.4.0: {}
-
balanced-match@1.0.2: {}
- better-ajv-errors@1.2.0(ajv@8.17.1):
+ better-ajv-errors@1.2.0(@redocly/ajv@8.17.1):
dependencies:
- '@babel/code-frame': 7.27.1
+ '@babel/code-frame': 7.28.6
'@humanwhocodes/momoa': 2.0.4
- ajv: 8.17.1
+ ajv: '@redocly/ajv@8.17.1'
chalk: 4.1.2
jsonpointer: 5.0.1
leven: 3.1.0
@@ -1546,11 +1408,6 @@ snapshots:
dependencies:
fill-range: 7.1.1
- call-bind-apply-helpers@1.0.2:
- dependencies:
- es-errors: 1.3.0
- function-bind: 1.1.2
-
call-me-maybe@1.0.2: {}
camelize@1.0.1: {}
@@ -1580,7 +1437,7 @@ snapshots:
parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2
- undici: 7.15.0
+ undici: 7.18.2
whatwg-mimetype: 4.0.0
chokidar@3.6.0:
@@ -1615,10 +1472,6 @@ snapshots:
colorette@2.0.20: {}
- combined-stream@1.0.8:
- dependencies:
- delayed-stream: 1.0.0
-
cookie@0.7.2: {}
core-js@3.45.1: {}
@@ -1649,7 +1502,7 @@ snapshots:
css-what@6.2.2: {}
- csstype@3.1.3: {}
+ csstype@3.2.3: {}
debug@4.4.3:
dependencies:
@@ -1657,10 +1510,6 @@ snapshots:
decko@1.2.0: {}
- delayed-stream@1.0.0: {}
-
- diff-sequences@29.6.3: {}
-
diff@4.0.2: {}
dom-serializer@2.0.0:
@@ -1675,7 +1524,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
- dompurify@3.2.7:
+ dompurify@3.3.1:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -1687,12 +1536,6 @@ snapshots:
dotenv@16.4.7: {}
- dunder-proto@1.0.1:
- dependencies:
- call-bind-apply-helpers: 1.0.2
- es-errors: 1.3.0
- gopd: 1.2.0
-
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
@@ -1708,21 +1551,6 @@ snapshots:
entities@6.0.1: {}
- es-define-property@1.0.1: {}
-
- es-errors@1.3.0: {}
-
- es-object-atoms@1.1.1:
- dependencies:
- es-errors: 1.3.0
-
- es-set-tostringtag@2.1.0:
- dependencies:
- es-errors: 1.3.0
- get-intrinsic: 1.3.0
- has-tostringtag: 1.0.2
- hasown: 2.0.2
-
es6-promise@3.3.1: {}
escalade@3.2.0: {}
@@ -1752,53 +1580,23 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
- form-data@4.0.4:
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- es-set-tostringtag: 2.1.0
- hasown: 2.0.2
- mime-types: 2.1.35
-
fsevents@2.3.3:
optional: true
- function-bind@1.1.2: {}
-
get-caller-file@2.0.5: {}
- get-intrinsic@1.3.0:
- dependencies:
- call-bind-apply-helpers: 1.0.2
- es-define-property: 1.0.1
- es-errors: 1.3.0
- es-object-atoms: 1.1.1
- function-bind: 1.1.2
- get-proto: 1.0.1
- gopd: 1.2.0
- has-symbols: 1.1.0
- hasown: 2.0.2
- math-intrinsics: 1.1.0
-
- get-proto@1.0.1:
- dependencies:
- dunder-proto: 1.0.1
- es-object-atoms: 1.1.1
-
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
- glob@11.0.3:
+ glob@11.1.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.1.1
- minimatch: 10.0.3
+ minimatch: 10.1.1
minipass: 7.1.2
package-json-from-dist: 1.0.1
- path-scurry: 2.0.0
-
- gopd@1.2.0: {}
+ path-scurry: 2.0.1
handlebars@4.7.8:
dependencies:
@@ -1811,16 +1609,6 @@ snapshots:
has-flag@4.0.0: {}
- has-symbols@1.1.0: {}
-
- has-tostringtag@1.0.2:
- dependencies:
- has-symbols: 1.1.0
-
- hasown@2.0.2:
- dependencies:
- function-bind: 1.1.2
-
htmlparser2@10.0.0:
dependencies:
domelementtype: 2.3.0
@@ -1863,27 +1651,11 @@ snapshots:
dependencies:
'@isaacs/cliui': 8.0.2
- jest-diff@29.7.0:
- dependencies:
- chalk: 4.1.2
- diff-sequences: 29.6.3
- jest-get-type: 29.6.3
- pretty-format: 29.7.0
-
- jest-get-type@29.6.3: {}
-
- jest-matcher-utils@29.7.0:
- dependencies:
- chalk: 4.1.2
- jest-diff: 29.7.0
- jest-get-type: 29.6.3
- pretty-format: 29.7.0
-
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {}
- js-yaml@4.1.0:
+ js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -1893,7 +1665,7 @@ snapshots:
json-schema-to-ts@2.7.2:
dependencies:
- '@babel/runtime': 7.28.4
+ '@babel/runtime': 7.28.6
'@types/json-schema': 7.0.15
ts-algebra: 1.2.2
@@ -1911,7 +1683,7 @@ snapshots:
dependencies:
js-tokens: 4.0.0
- lru-cache@11.2.2: {}
+ lru-cache@11.2.4: {}
lunr@2.3.9: {}
@@ -1921,15 +1693,7 @@ snapshots:
marked@4.3.0: {}
- math-intrinsics@1.1.0: {}
-
- mime-db@1.52.0: {}
-
- mime-types@2.1.35:
- dependencies:
- mime-db: 1.52.0
-
- minimatch@10.0.3:
+ minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
@@ -1941,21 +1705,21 @@ snapshots:
minipass@7.1.2: {}
- mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
mobx: 6.15.0
- react: 19.2.0
- use-sync-external-store: 1.6.0(react@19.2.0)
+ react: 19.2.3
+ use-sync-external-store: 1.6.0(react@19.2.3)
optionalDependencies:
- react-dom: 19.2.0(react@19.2.0)
+ react-dom: 19.2.3(react@19.2.3)
- mobx-react@9.2.0(mobx@6.15.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ mobx-react@9.2.0(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
mobx: 6.15.0
- mobx-react-lite: 4.1.1(mobx@6.15.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- react: 19.2.0
+ mobx-react-lite: 4.1.1(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
optionalDependencies:
- react-dom: 19.2.0(react@19.2.0)
+ react-dom: 19.2.3(react@19.2.3)
mobx@6.15.0: {}
@@ -2016,7 +1780,7 @@ snapshots:
object-assign@4.1.1: {}
- openapi-sampler@1.6.1:
+ openapi-sampler@1.6.2:
dependencies:
'@types/json-schema': 7.0.15
fast-xml-parser: 4.5.3
@@ -2043,9 +1807,9 @@ snapshots:
path-key@3.1.1: {}
- path-scurry@2.0.0:
+ path-scurry@2.0.1:
dependencies:
- lru-cache: 11.2.2
+ lru-cache: 11.2.4
minipass: 7.1.2
perfect-scrollbar@1.5.6: {}
@@ -2054,11 +1818,13 @@ snapshots:
picomatch@2.3.1: {}
+ picomatch@4.0.3: {}
+
pluralize@8.0.0: {}
polished@4.3.1:
dependencies:
- '@babel/runtime': 7.28.4
+ '@babel/runtime': 7.28.6
postcss-value-parser@4.2.0: {}
@@ -2068,12 +1834,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- pretty-format@29.7.0:
- dependencies:
- '@jest/schemas': 29.6.3
- ansi-styles: 5.2.0
- react-is: 18.3.1
-
prismjs@1.30.0: {}
prop-types@15.8.1:
@@ -2094,7 +1854,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
- '@types/node': 24.3.1
+ '@types/node': 24.10.7
long: 5.3.2
queue-microtask@1.2.3: {}
@@ -2103,22 +1863,20 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
- react-dom@19.2.0(react@19.2.0):
+ react-dom@19.2.3(react@19.2.3):
dependencies:
- react: 19.2.0
+ react: 19.2.3
scheduler: 0.27.0
react-is@16.13.1: {}
- react-is@18.3.1: {}
-
- react-tabs@6.1.0(react@19.2.0):
+ react-tabs@6.1.0(react@19.2.3):
dependencies:
clsx: 2.1.1
prop-types: 15.8.1
- react: 19.2.0
+ react: 19.2.3
- react@19.2.0: {}
+ react@19.2.3: {}
readable-stream@3.6.2:
dependencies:
@@ -2130,32 +1888,32 @@ snapshots:
dependencies:
picomatch: 2.3.1
- redoc@2.5.1(core-js@3.45.1)(mobx@6.15.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)):
+ redoc@2.5.1(core-js@3.45.1)(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(styled-components@6.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
dependencies:
- '@redocly/openapi-core': 1.34.5
+ '@redocly/openapi-core': 1.34.6
classnames: 2.5.1
core-js: 3.45.1
decko: 1.2.0
- dompurify: 3.2.7
+ dompurify: 3.3.1
eventemitter3: 5.0.1
json-pointer: 0.6.2
lunr: 2.3.9
mark.js: 8.11.1
marked: 4.3.0
mobx: 6.15.0
- mobx-react: 9.2.0(mobx@6.15.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- openapi-sampler: 1.6.1
+ mobx-react: 9.2.0(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ openapi-sampler: 1.6.2
path-browserify: 1.0.1
perfect-scrollbar: 1.5.6
polished: 4.3.1
prismjs: 1.30.0
prop-types: 15.8.1
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- react-tabs: 6.1.0(react@19.2.0)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-tabs: 6.1.0(react@19.2.3)
slugify: 1.4.7
stickyfill: 1.1.1
- styled-components: 6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ styled-components: 6.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
swagger2openapi: 7.0.8
url-template: 2.0.8
transitivePeerDependencies:
@@ -2177,7 +1935,7 @@ snapshots:
semver@7.7.3: {}
- set-cookie-parser@2.7.1: {}
+ set-cookie-parser@2.7.2: {}
shallowequal@1.1.0: {}
@@ -2261,21 +2019,21 @@ snapshots:
strnum@1.1.2: {}
- styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ styled-components@6.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- '@emotion/is-prop-valid': 1.2.2
- '@emotion/unitless': 0.8.1
- '@types/stylis': 4.2.5
+ '@emotion/is-prop-valid': 1.4.0
+ '@emotion/unitless': 0.10.0
+ '@types/stylis': 4.2.7
css-to-react-native: 3.2.0
- csstype: 3.1.3
+ csstype: 3.2.3
postcss: 8.4.49
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
shallowequal: 1.1.0
- stylis: 4.3.2
- tslib: 2.6.2
+ stylis: 4.3.6
+ tslib: 2.8.1
- stylis@4.3.2: {}
+ stylis@4.3.6: {}
supports-color@7.2.0:
dependencies:
@@ -2305,44 +2063,44 @@ snapshots:
ts-algebra@1.2.2: {}
- ts-node@10.9.2(@types/node@24.3.1)(typescript@5.9.2):
+ ts-node@10.9.2(@types/node@24.10.7)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
- '@tsconfig/node10': 1.0.11
+ '@tsconfig/node10': 1.0.12
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
- '@types/node': 24.3.1
+ '@types/node': 24.10.7
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
- typescript: 5.9.2
+ typescript: 5.9.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
- tslib@2.6.2: {}
+ tslib@2.8.1: {}
- typescript@5.9.2: {}
+ typescript@5.9.3: {}
uglify-js@3.19.3:
optional: true
- undici-types@7.10.0: {}
+ ulid@3.0.2: {}
- undici@6.22.0: {}
+ undici-types@7.16.0: {}
- undici@7.15.0: {}
+ undici@6.23.0: {}
- uri-js-replace@1.0.1: {}
+ undici@7.18.2: {}
url-template@2.0.8: {}
- use-sync-external-store@1.6.0(react@19.2.0):
+ use-sync-external-store@1.6.0(react@19.2.3):
dependencies:
- react: 19.2.0
+ react: 19.2.3
util-deprecate@1.0.2: {}