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: {}