The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
197 lines
7.1 KiB
Go
197 lines
7.1 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package commonthirdparties
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.gearno.de/kit/pg"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/gid"
|
|
"go.probo.inc/probo/pkg/proboctl/cmdutil"
|
|
"go.probo.inc/probo/pkg/slug"
|
|
)
|
|
|
|
//go:embed data/data.json
|
|
var dataJSON []byte
|
|
|
|
type thirdPartyData struct {
|
|
Name string `json:"name"`
|
|
Category *string `json:"category,omitempty"`
|
|
HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
|
|
LegalName *string `json:"legalName,omitempty"`
|
|
WebsiteURL *string `json:"websiteUrl,omitempty"`
|
|
PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"`
|
|
ServiceLevelAgreementURL *string `json:"serviceLevelAgreementUrl,omitempty"`
|
|
ServiceSoftwareAgreementURL *string `json:"serviceSoftwareAgreementUrl,omitempty"`
|
|
DataProcessingAgreementURL *string `json:"dataProcessingAgreementUrl,omitempty"`
|
|
BusinessAssociateAgreementURL *string `json:"businessAssociateAgreementUrl,omitempty"`
|
|
SubprocessorsListURL *string `json:"subprocessorsListUrl,omitempty"`
|
|
Certifications []string `json:"certifications,omitempty"`
|
|
StatusPageURL *string `json:"statusPageUrl,omitempty"`
|
|
TermsOfServiceURL *string `json:"termsOfServiceUrl,omitempty"`
|
|
SecurityPageURL *string `json:"securityPageUrl,omitempty"`
|
|
TrustPageURL *string `json:"trustPageUrl,omitempty"`
|
|
Domains []string `json:"domains,omitempty"`
|
|
}
|
|
|
|
func NewCmdCommonThirdParties(f *cmdutil.Factory) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "common-third-parties",
|
|
Short: "Seed common third parties",
|
|
Long: "Seed the common_third_parties table from the embedded dataset. " +
|
|
"Re-running is safe: existing rows are upserted on conflict (slug) " +
|
|
"so ids and created_at are preserved.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
out := f.IOStreams.Out
|
|
errOut := f.IOStreams.ErrOut
|
|
ctx := cmd.Context()
|
|
|
|
thirdParties, err := loadThirdParties()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot load third-party data: %w", err)
|
|
}
|
|
|
|
pgClient, err := f.PgClient()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create pg client: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(out, "seeding %d common third parties\n", len(thirdParties))
|
|
|
|
var inserted, updated, domainsInserted, domainsUpdated int
|
|
|
|
if err := pgClient.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, tx pg.Tx) error {
|
|
now := time.Now()
|
|
|
|
for _, tp := range thirdParties {
|
|
party := coredata.CommonThirdParty{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
|
|
Name: tp.Name,
|
|
Slug: slug.Make(tp.Name),
|
|
Category: parseCategory(errOut, tp),
|
|
HeadquarterAddress: tp.HeadquarterAddress,
|
|
LegalName: tp.LegalName,
|
|
WebsiteURL: tp.WebsiteURL,
|
|
PrivacyPolicyURL: tp.PrivacyPolicyURL,
|
|
ServiceLevelAgreementURL: tp.ServiceLevelAgreementURL,
|
|
ServiceSoftwareAgreementURL: tp.ServiceSoftwareAgreementURL,
|
|
DataProcessingAgreementURL: tp.DataProcessingAgreementURL,
|
|
BusinessAssociateAgreementURL: tp.BusinessAssociateAgreementURL,
|
|
SubprocessorsListURL: tp.SubprocessorsListURL,
|
|
Certifications: tp.Certifications,
|
|
StatusPageURL: tp.StatusPageURL,
|
|
TermsOfServiceURL: tp.TermsOfServiceURL,
|
|
SecurityPageURL: tp.SecurityPageURL,
|
|
TrustPageURL: tp.TrustPageURL,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
wasInserted, err := party.Upsert(ctx, tx)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot upsert common third party %q: %w", tp.Name, err)
|
|
}
|
|
|
|
if wasInserted {
|
|
inserted++
|
|
} else {
|
|
updated++
|
|
|
|
if err := party.LoadByName(ctx, tx, tp.Name); err != nil {
|
|
return fmt.Errorf("cannot reload common third party %q: %w", tp.Name, err)
|
|
}
|
|
}
|
|
|
|
for _, domain := range tp.Domains {
|
|
d := coredata.CommonThirdPartyDomain{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyDomainEntityType),
|
|
CommonThirdPartyID: party.ID,
|
|
Domain: domain,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
domainInserted, err := d.Upsert(ctx, tx)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot upsert domain %q for %q: %w", domain, tp.Name, err)
|
|
}
|
|
|
|
if domainInserted {
|
|
domainsInserted++
|
|
} else {
|
|
domainsUpdated++
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(out, "seeded %d third parties (%d inserted, %d updated)\n", len(thirdParties), inserted, updated)
|
|
_, _ = fmt.Fprintf(out, "seeded %d domains (%d inserted, %d updated)\n", domainsInserted+domainsUpdated, domainsInserted, domainsUpdated)
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func loadThirdParties() ([]thirdPartyData, error) {
|
|
var thirdParties []thirdPartyData
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(dataJSON))
|
|
dec.DisallowUnknownFields()
|
|
|
|
if err := dec.Decode(&thirdParties); err != nil {
|
|
return nil, fmt.Errorf("cannot decode embedded data.json: %w", err)
|
|
}
|
|
|
|
return thirdParties, nil
|
|
}
|
|
|
|
func parseCategory(errOut io.Writer, tp thirdPartyData) coredata.ThirdPartyCategory {
|
|
if tp.Category == nil || *tp.Category == "" {
|
|
return coredata.ThirdPartyCategoryOther
|
|
}
|
|
|
|
var c coredata.ThirdPartyCategory
|
|
if err := c.UnmarshalText([]byte(*tp.Category)); err != nil {
|
|
_, _ = fmt.Fprintf(errOut, "warning: third party %q has unknown category %q, falling back to OTHER\n", tp.Name, *tp.Category)
|
|
return coredata.ThirdPartyCategoryOther
|
|
}
|
|
|
|
return c
|
|
}
|