@push.rocks/smartlinuxrepo

A pure codec for Linux package repositories: Alpine (apk), Debian (apt), RPM (dnf) and Arch (pacman). It reads package files into JSON records, builds repository index file sets and signs them, following Gitea 28.2.0. It does no storage and no network I/O; the caller owns both.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add @push.rocks/smartlinuxrepo

Node.js ^24.20.0 || >=26.6.0: the releases whose node:zlib reports a truncated zstd frame (nodejs/node#64593). On an older Node, opening a zstd decoder throws a plain Error.

Status

All four formats read, index and sign: readAlpinePackage, readDebianPackage, readRpmPackage and readArchPackage read packages into records; buildAlpineIndex, buildDebianIndex, buildRpmIndex and buildArchIndex build signed index file sets; signRpmPackage and signArchPackage sign packages at upload. Each format is checked against Gitea 28.2.0's own output and installed from with apk, apt, dnf and pacman. createOpenPgpVerifier verifies OpenPGP detached and cleartext signatures (see Verification). The index readers read served index documents back into units, and createAlpineVerifier verifies APKINDEX.tar.gz signatures (see Index readers). The built-in OpenPGP signer does not stream: openpgp.js 6.3.2 holds a detached-signed document in memory (see Signing).

Records

TLinuxPackageRecord is a union of IAlpinePackageRecord, IDebianPackageRecord, IRpmPackageRecord and IArchPackageRecord, discriminated by format. Every record carries name, version, architecture, fileName, digests (size, md5, sha1, sha256, sha512 over the whole file; Alpine adds controlSha1) and a format-specific metadata object. Records are plain JSON and safe to persist; changing their shape is a major release.

Limits and errors

Readers take Partial<ILinuxPackageLimits> per call over DEFAULT_LINUX_PACKAGE_LIMITS:

Limit Default Bounds
maxPackageBytes 8 GiB the whole input
maxMetadataBytes 64 MiB one buffered metadata unit (apk or deb control member, RPM headers, an RPM errata request body)
maxDecompressedMetadataBytes 32 MiB a decompressed control tar or .PKGINFO
maxPayloadBytes 16 GiB the decompressed Arch payload walked for the file list
maxEntries 100,000 archive entries, Zstandard frames per zstd stream, values per multi-valued field, elements of one JSON array or members of one JSON object, and updates, collections, packages and references each per RPM errata body
maxFileListBytes 16 MiB total Arch file-name bytes
maxHeaderBytes 4 MiB one tar header chain or metadata line
maxIndexBytes 256 MiB one decoded index document (index readers only)

Every failure is a LinuxRepoError with a stable code (limit-exceeded, unsupported-compression, malformed-archive, malformed-header, missing-field, invalid-field, unsupported-package-format, truncated, digest-mismatch, invalid-input, invalid-key, signing-failed, from the verifiers malformed-signature, wrong-key, unusable-key, bad-signature, from the index readers malformed-index, and from readRpmErrataBody malformed-json) and a detail naming the field or limit. Errors from your own source or signer, and failures inside node:crypto or openpgp.js during key generation, are passed through unchanged. New codes arrive in minor releases (1.1.0 added the four verifier codes, 1.2.0 malformed-index, 1.3.0 malformed-json), each raised only by a new function: a switch over code needs a default branch.

Compressed streams (a deb control member, an Arch package) are read to their end: concatenated gzip members and zstd frames are decoded, zstd skippable frames are skipped, and any other bytes after the last gzip member or zstd frame are malformed-archive.

Signing

The codec never stores keys. You pass a signer on every call:

import {
  createAlpineSigner,
  createOpenPgpSigner,
  generateAlpineKeyPair,
  generateOpenPgpKeyPair,
  giteaAlpineKeyName,
} from '@push.rocks/smartlinuxrepo';

const alpineKeys = await generateAlpineKeyPair(); // RSA-4096, PKCS#1 private + PKIX public PEM
const alpineSigner = createAlpineSigner({
  privateKeyPem: alpineKeys.privateKeyPem,
  keyName: giteaAlpineKeyName('my-org', alpineKeys.publicKeyPem), // my-org@<sha256>.rsa.pub
});

const pgpKeys = await generateOpenPgpKeyPair({ userId: 'Example Repository Key', timestamp: new Date() });
const pgpSigner = await createOpenPgpSigner({ privateKeyArmored: pgpKeys.privateKeyArmored });

IAlpineSigner.sign returns an RSA PKCS#1 v1.5 SHA-1 signature. IOpenPgpSigner.signBinary and signText return one raw OpenPGP v4 signature packet (SHA-256, created at the given timestamp). signBinary takes the document as an AsyncIterable<Uint8Array> and signs the concatenation of its chunks, reading it to its end exactly once, so a whole package can be signed while it streams; pass bytes you already hold as a single chunk. The built-in signer from createOpenPgpSigner does not stream yet: openpgp.js 6.3.2 keeps every chunk of a detached-signed stream and then concatenates them, so signing peaks at about twice the document size in memory. A custom IOpenPgpSigner implementation can stream. The codec does all armoring and cleartext framing, so any implementation of the interface (an HSM, a remote signer, a test stub) produces identically framed files. createOpenPgpSigner accepts unencrypted v4 keys only.

isIndexableArchitecture(architecture) tells whether an architecture can name an index path: not empty, . or .., and without /, whitespace or control characters. Builders refuse entries that fail it (invalid-input), and every package reader refuses a package whose architecture fails it with invalid-field (detail.field is Architecture for Debian, ARCH for RPM, arch for Alpine and Arch; an empty Debian Architecture is missing-field), so no reader returns a record its builder refuses. Gitea accepts more: any Alpine architecture, the empty one included, any non-empty Debian or Arch architecture, and an unchecked RPM ARCH.

Verification

import { createOpenPgpVerifier } from '@push.rocks/smartlinuxrepo';

const verifier = await createOpenPgpVerifier({ publicKeyArmored }); // invalid-key unless one v4 public key
const now = new Date();
const signature = await pgpSigner.signBinary((async function* () { yield data; })(), now); // a raw packet, as an Arch .sig
await verifier.verifyDetachedSignature({ data, signature, date: now });
await verifier.verifyArmoredDetachedSignature({ data: release, signatureArmored: releaseGpg, date: now }); // Release.gpg, repomd.xml.asc
const { signedText } = await verifier.verifyCleartextMessage({ message: inRelease, date: now }); // InRelease

createOpenPgpVerifier reads exactly one transferable OpenPGP v4 public key (primary key, user IDs, subkeys, self-signatures) in exactly one PUBLIC KEY BLOCK. A private key, several blocks or keys, a v6 key, text around the block or an armor CRC-24 that does not match is invalid-key. Armor headers (Comment:, Version:) are ignored; a malformed header line is refused. Whether the key can sign is checked at verification, where the time is known.

The three methods take the served bytes as Uint8Array and a required date; the codec reads no clock. verifyDetachedSignature takes a raw signature packet (what signBinary returns, an Arch .sig), verifyArmoredDetachedSignature an ASCII-armored one, and nothing is sniffed. A signature is valid when it is exactly one v4 signature packet (type 0x00 detached, 0x01 cleartext) hashed with SHA-256, SHA-384 or SHA-512, issued by the primary key or one of its subkeys, created at or before date and not expired at date, made by a key that can sign both at the signature's creation time and at date (not expired, not revoked, sign flag, valid binding, openpgp.js's algorithm floor), and when it verifies. A result names the key or subkey that verified (signingKeyFingerprint, 40 uppercase hex), the creation time and the hash algorithm. Every failure is a LinuxRepoError, never a boolean:

Code Meaning
malformed-signature armor, encoding or framing, not exactly one v4 signature packet, the wrong signature type, a hash outside SHA-256/384/512, a Hash: header without the signature's hash, no creation time, no or a wildcard issuer, an unknown critical subpacket or notation
wrong-key issued by neither the primary key nor a subkey, or the issuer fingerprint disagrees (detail.value: the issuer in hex)
unusable-key the issuing key or subkey cannot sign at the creation time or at date: expired, revoked, no sign flag, created after the signature, below the algorithm floor (detail.value: the fingerprint of the key or subkey that cannot sign)
bad-signature the cryptographic check failed, or the signature was created after date or has expired at date
invalid-input an argument that is not a Uint8Array, or an invalid date

A cleartext message is strict UTF-8 with LF line endings, as apt splits it: the -----BEGIN PGP SIGNED MESSAGE----- line first, optional Hash: headers and no others, the dash-escaped text, one signature block, then at most one line break. Text before or after, a CR, a line ending in a space or tab, or a line starting with - but not - is malformed-signature. signedText is the text as signed: dash-escaping removed, without the line break before the signature block. For the codec's and GnuPG's InRelease that is Release without its final line break; Gitea 28.2.0's InRelease (go-crypto) signs that line break too, so its signedText is the whole Release.

Two refusals depart from RFC 9580: an armor CRC-24 that does not match (§6.1; gpgv refuses it, sqv accepts it) and a Hash: header that does not name the signature's hash (§6.2.2.3).

The verdicts match gpgv as apt reads it (EXPKEYSIG and REVKEYSIG are refusals). sqv, apt 3's verifier, checks the key only at the signature's creation time: it accepts a signature whose subkey expired, or whose key was soft-revoked, after signing, which the codec refuses at a later date.

Every builder returns the complete file set for its input as ILinuxIndexFile[] (path, bytes, sha256, mediaType, stage, aliases); a previously written file that is not in the set is stale. Write every content file before any pointer file: the pointer files are the signed documents that name the content files.

Index readers

The index readers read served index documents back into units, for comparing two producers' indexes (the codec's and Gitea's, say) as multisets of units. A unit is its key, its exact bytes as served (never a re-rendering) and a small lexical view; the parts outside the units come back as raw bytes too.

import {
  createAlpineVerifier,
  readAlpineIndex,
  readArchIndex,
  readDebianPackagesIndex,
  readDebianRelease,
  readRpmRepodata,
  readRpmRepomd,
  readRpmUpdateInfo,
  stripDebianIndexOwnedFields,
} from '@push.rocks/smartlinuxrepo';

const packages = await readDebianPackagesIndex({ bytes: packagesXz, compression: 'xz' });
for (const stanza of packages.stanzas) {
  stanza.filename; // the key: the last Filename
  stanza.bytes; // the stanza's exact bytes
  stripDebianIndexOwnedFields(stanza.bytes); // without Filename, Size, MD5sum, SHA1, SHA256, SHA512
}
const release = await readDebianRelease({ bytes: releaseBytes }); // fields in order, files.{md5Sum,sha1,sha256,sha512}
const primary = await readRpmRepodata({ bytes: primaryGz, compression: 'gzip', document: 'primary' }); // packages by pkgid
const apkindex = await readAlpineIndex({ bytes: apkindexTarGz }); // signatures, signedBytes, members, blocks
await createAlpineVerifier({ publicKeyPem, keyName }).verifyIndexSignature({ index: apkindexTarGz });
const db = await readArchIndex({ bytes: packagesDb, compression: 'gzip' }); // desc and files members by name
Reader Document Units and key Also returned
readDebianPackagesIndex Packages, .gz, .xz stanzas; the last Filename decoded; package, version, architecture
readDebianRelease Release fields in order files.md5Sum, sha1, sha256, sha512 as { path, size, digest }
readAlpineIndex APKINDEX.tar.gz APKINDEX blocks; P:, V:, A: the .SIGN.* entries, the signed bytes, the tar members
readRpmRepodata primary, filelists, other <package>; pkgid decoded, the XML frame, name, arch, epoch/version/release, <size>, files
readRpmUpdateInfo updateinfo <update>; <id> decoded, the XML frame, the package list
readRpmRepomd repomd.xml <data>; type the XML frame, other root elements (<revision>, <tags>) raw
readArchIndex packages.db tar members; name decoded
  • Every reader takes { bytes: Uint8Array, ..., limits?: Partial<ILinuxPackageLimits> } and returns a Promise. The compression ('none' | 'gzip' | 'xz' | 'zstd', TLinuxCompression) and the RPM document are named by the caller from the file name; nothing is sniffed. A stream with bytes after its last gzip member or zstd frame is malformed-archive.
  • A reader fails the whole document with a LinuxRepoError and never returns a partial result. Every unit is returned in document order, duplicates included (two stanzas with one Filename, two packages with one pkgid); nothing is merged.
  • Returned units are views into one buffer the reader allocated (decoded); offset indexes into it. readAlpineIndex has no decoded: its units are views into the APKINDEX member, and its signedBytes and signature bytes are its own copies. No result aliases your input.
  • Bounds: the decoded document by maxIndexBytes, units (and values per parsed list, open XML elements, Zstandard frames of a zstd document) by maxEntries, one unit by maxMetadataBytes, one line or XML start tag by maxHeaderBytes.
  • Packages and Release lines end in LF, the last one too; a line holding only spaces or tabs is malformed-index. A Release holds one stanza: no empty line, checksum lines of a digest, a size and a path separated by spaces or tabs (invalid-field otherwise). To read an InRelease, verify it with createOpenPgpVerifier().verifyCleartextMessage and pass its signedText to readDebianRelease, adding a final LF when it lacks one.
  • RPM documents are read by a strict XML scanner: UTF-8 only, no DOCTYPE (so no entities), well-formed, the root the document names, only whitespace between units (not returned). The frame (prolog, rootStartTag, rootAttributes, namespace, rootEndTag, trailer) is returned as written; for Gitea's and the codec's documents the frame and the units rebuild decoded. isRpmPrimaryPath is createrepo_c's cr_is_primary, the rule the codec adds to Gitea's primary file list.
  • readAlpineIndex splits the index as apk does: the first gzip member is the signature member when its tar's first entry is named .SIGN.*. createAlpineVerifier({ publicKeyPem, keyName }) takes one PKIX PUBLIC KEY PEM (what Gitea serves at /alpine/key and Alpine installs in /etc/apk/keys) of an RSA key of at least 2047 bits and the key name apk finds it under, and verifies RSA PKCS#1 v1.5 over SHA-1 of the signed bytes. It fails closed: malformed-signature (unsigned, several .SIGN. entries, a type other than RSA), wrong-key (another key name, detail.value), bad-signature.
  • readArchIndex returns regular files and skips directory entries (pacman's repo-add writes them); desc and files are not parsed. compareArchVersions is pacman's vercmp.
  • malformed-index is an index document a reader refuses: line framing, block or stanza structure, XML well-formedness, an unexpected root or child element, an ambiguous duplicate key line or element (P: twice, two <id>), or a tar entry type an index does not hold. A unit without its key is missing-field.

Alpine

import { buildAlpineIndex, readAlpinePackage } from '@push.rocks/smartlinuxrepo';

// upload() returns a fresh AsyncIterable<Uint8Array> of the package bytes; limits are optional
const record = await readAlpinePackage(upload(), limits);
const files = await buildAlpineIndex({
  branch: 'v3.22',
  repository: 'main',
  entries: [record], // every package of this branch and repository
  signer: alpineSigner,
  timestamp: new Date(),
});

readAlpinePackage reads apk v2 packages, signed or unsigned; an apk v3 (ADB) package is unsupported-package-format, as in Gitea. The record's fileName is <name>-<version>.apk, and digests.controlSha1 is the SHA-1 of the whole control gzip member, which is what apk verifies (the index's C: value). An arch that fails isIndexableArchitecture, a missing one included, is invalid-field (detail.field arch), where Gitea stores any value.

For every architecture the entries name, buildAlpineIndex returns one file, <branch>/<repository>/<architecture>/APKINDEX.tar.gz: a gzip member with the signature entry .SIGN.RSA.<keyName> (the signer's keyName), followed by the gzip member holding APKINDEX. Packages of architecture noarch are also listed in every other architecture's index, and every entry's A: is the index's architecture, so apk requests a noarch package under the architecture's path. Your route serves package files at <branch>/<repository>/<architecture>/<fileName> and, as Gitea 28.2.0 does:

  • A package file or APKINDEX.tar.gz that does not exist under the requested architecture is looked up under noarch. A repository with only noarch packages has no other index, so apk gets the noarch one.
  • apk finds the public key by the signature entry's name: users install it as /etc/apk/keys/<keyName>. giteaAlpineKeyName gives Gitea's name, <owner>@<sha256 of the public key>.rsa.pub.

Debian

import { buildDebianIndex, readDebianPackage } from '@push.rocks/smartlinuxrepo';

// upload() returns a fresh AsyncIterable<Uint8Array> of the package bytes; limits are optional
const record = await readDebianPackage(upload(), limits);
const files = await buildDebianIndex({
  distribution: 'bookworm',
  entries: [{ component: 'main', record }], // every package of the distribution
  origin: 'Example',
  label: 'Example',
  signer: pgpSigner,
  timestamp: new Date(),
});

readDebianPackage reads the control.tar member (plain, .gz, .xz or .zst) and hashes the rest of the file without decompressing it. The record keeps Gitea's derived values exactly, and its fileName is <name>_<version>_<architecture>.deb. Two departures from Gitea's control checks keep every record indexable. The version: a version that holds / or ? is refused with invalid-field (detail.field Version), a departure from Gitea's version pattern, which admits both in the revision (its +-~ class is the range 0x2B-0x7E); no pool file name can hold them, because / splits the path and apt sends ? unescaped, so the server would read the rest as a query. The architecture: Gitea accepts any non-empty Architecture; the reader refuses one that is . or .. or holds /, #, ?, whitespace or a control character (invalid-field, detail.field Architecture), because the file name embeds it. buildDebianIndex applies the same rules to record.architecture and record.fileName (invalid-input), and also refuses a hand-built record.fileName of . or .., so every record the reader returns can be indexed.

buildDebianIndex returns the complete set for one distribution. For every component and architecture with packages it writes dists/<distribution>/<component>/binary-<architecture>/Packages, Packages.gz and Packages.xz (content files). Packages of architecture all are listed only under binary-all, as in Gitea. Then it writes dists/<distribution>/Release, its armored detached signature Release.gpg and the cleartext-signed InRelease (pointer files). distribution and component must match ^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$ (isDebianSymbol); origin and label are each one non-empty line without surrounding whitespace (isDebianReleaseValue). Both predicates are the builder's own rules, for checking configuration before a build. Entries are IDebianIndexEntry objects ({ component, record }). Your route serves:

  • every file at its path and at each of its aliases, by-hash/MD5Sum, SHA1, SHA256 and SHA512/<hex> next to the Packages file. Release sets Acquire-By-Hash: yes, and apt 2.6 and 3.x fetch Packages.xz through by-hash/SHA512/<hex>;
  • each package file at pool/<distribution>/<component>/<fileName>, the Filename: its stanza names;
  • the public key (publicKeyArmored from generateOpenPgpKeyPair), which users install as /etc/apt/keyrings/<name>.asc and reference with signed-by.

RPM

import { buildRpmIndex, mergeRpmUpdates, readRpmPackage, renderRpmRepoConfig, signRpmPackage } from '@push.rocks/smartlinuxrepo';

// optional upload signing, as Gitea's ?sign=true: tags 1002 (header + payload) and 268 (header)
const { prefix, resumeAt } = await signRpmPackage({ source: packageStream(), signer: pgpSigner, timestamp: new Date() });
// the signed package is `prefix` followed by the original bytes from `resumeAt`

const record = await readRpmPackage(signedPackageStream());
const files = await buildRpmIndex({
  group: 'el/9', // '' for the root group
  entries: [record],
  updates: [], // IRpmUpdate[] of every version with a file in the group
  repodataNaming: 'plain', // Gitea's repodata/<type>.xml.gz, or 'content-addressed' for <sha256>-<type>.xml.gz
  signer: pgpSigner,
  timestamp: new Date(),
});

buildRpmIndex returns primary, filelists, other (and updateinfo when there are updates) as .xml.gz content files, then repomd.xml and repomd.xml.asc. The documents follow Gitea 28.2.0 with three corrections: the filelists namespace is http://linux.duke.edu/metadata/filelists, primary also lists the paths createrepo_c's cr_is_primary selects (/etc/…, *bin/*, /usr/lib/sendmail), and every document lists a source RPM's architecture as src (its package path keeps the ARCH tag). readRpmPackage refuses with invalid-field a NAME or [EPOCH-]VERSION-RELEASE that cannot name a path segment (empty, ., .., / or a control character), and an ARCH that fails isIndexableArchitecture (detail.field ARCH), which Gitea does not check. mergeRpmUpdates merges posted errata as Gitea's errata route does; renderRpmRepoConfig writes Gitea's .repo text, optionally with repo_gpgcheck.

Errata (1.3.0)

Gitea's POST {group}/package/{name}/{version}/errata route takes a JSON array of advisories whose wire names differ from IRpmUpdate (pkg_list, filename, issued: { date }). readRpmErrataBody reads that body exactly as Gitea 28.2.0 decodes it (encoding/json/v2 defaults, then the route's null filter) and returns IRpmUpdate[] for mergeRpmUpdates:

import { LinuxRepoError, mergeRpmUpdates, readRpmErrataBody } from '@push.rocks/smartlinuxrepo';

const incoming = readRpmErrataBody({ bytes: requestBody }); // synchronous; decode before looking the version up
const updates = mergeRpmUpdates({ existing: storedUpdates, incoming, timestamp: new Date() });
  • Updates come back in body order, duplicate ids kept (mergeRpmUpdates merges them in order, as Gitea does); a body of null or [] is [] (answer 200 without a change). Each result is a new plain object with exactly the IRpmUpdate keys: pkg_list becomes collections, filename fileName; issued/updated absent or null is null, otherwise its date ("" when absent or null); absent or null strings are "" and arrays []; null elements of the top-level array, pkg_list, packages and references are skipped.
  • Names match exactly: ID, Pkg_List or pkglist are unknown members and ignored, as are other unknown members of any valid JSON. Refused, as Gitea refuses them: a duplicate member name in any object (also "id" and "\u0069d"), invalid UTF-8, a lone surrogate escape, a byte order mark, an empty or whitespace-only body, data after the value, invalid JSON, more than 10,000 nested arrays and objects (malformed-json), and a known member or element of the wrong JSON kind (invalid-field). detail.field is the JSON Pointer Go reports, with Gitea's names (/0/pkg_list/1/short, "" for the top level), and detail.value the byte offset Go reports; decoding stops at the first error, in Go's order.
  • Bounds Gitea does not have: a body longer than maxMetadataBytes is limit-exceeded before it is read; an array or object at any depth with more than maxEntries elements or members is limit-exceeded (detail.field its pointer); and so are updates, collections, packages and references each per RPM errata body: a body holds at most maxEntries objects of each kind, and the next one is limit-exceeded (detail.field the pointer of that object, such as /0/pkg_list/1/packages/0). null elements, unknown members and issued/updated objects do not count; when an element crosses both bounds, its container's is reported. null elements of references are skipped where Gitea stores them; Gitea's updateinfo.xml omits them, so the document is the same.
  • The path's name and version are not inputs: Gitea never compares the body with them. Answer limit-exceeded on maxMetadataBytes with 413 and every other LinuxRepoError of this function with 400, except invalid-input (your own argument, 500).

Arch

import { buildArchIndex, readArchPackage, signArchPackage } from '@push.rocks/smartlinuxrepo';

// upload() returns a fresh AsyncIterable<Uint8Array> of the package bytes; limits are optional
const record = await readArchPackage(upload(), limits);
const packageSignature = await signArchPackage({ source: upload(), signer: pgpSigner, timestamp: new Date(), limits });
const files = await buildArchIndex({ repository: 'core', entries: [{ record, packageSignature }], signer: pgpSigner, timestamp: new Date() });

readArchPackage refuses an arch that fails isIndexableArchitecture with invalid-field (detail.field arch), where Gitea refuses only an empty one. Pass signArchPackage the limits the package was read with. Store its result with the package: it is the package's .sig file, and the builder writes it into the index as %PGPSIG%. An entry without one has no %PGPSIG%, and pacman fetches <file>.sig instead.

For every architecture that has packages, buildArchIndex returns two files: <repository>/<architecture>/packages.db (a gzip tar) and its signature packages.db.sig. Packages of architecture any also go into every other architecture's db. The files have no aliases. Your route maps request names onto them, as Gitea 28.2.0 does for GET <repository>/<architecture>/<filename>:

  • A trailing .sig asks for the signature of the rest of the name.
  • A name ending in .db, .db.tar.gz, .files or .files.tar.gz means packages.db. pacman requests <repo>.db, where <repo> is the repository's name in pacman.conf.
  • A name that does not exist under the requested architecture is looked up under any. Gitea does this for every name, so a package uploaded as any downloads from every architecture's URL.

This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.

Company Information

Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany

For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme MIT
2 MiB
0 Stars 3 Watchers 0 Forks
Languages
TypeScript 92.9%
Go 6.3%
Shell 0.8%