Skip to content

Commit

Permalink
Add support for considering oss authors/contribs as indirect sponsors
Browse files Browse the repository at this point in the history
This feature effectively makes anyone who has ever contributed to an active (and popular) open source nuget package, an indirect sponsor that doesn't need to select a sponsorship tier with the sponsorable.

This works in combination with the https://github.com/devlooped/nuget repository which will keep a public dump of the stats we collect once a month (first saturday of each month) with all the active packages on nuget.org and their github mapping and repo contributors.

We provide a lookup page so users can quickly determine eligibility too, if they don't wish to sponsor. We'll link to that page from our diagnostic URL.

We only do this for github repositories at the moment.
  • Loading branch information
kzu committed Sep 26, 2024
1 parent 6e3805a commit b6d9955
Show file tree
Hide file tree
Showing 19 changed files with 472 additions and 150 deletions.
5 changes: 0 additions & 5 deletions .netconfig
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,6 @@
sha = 2e84192eea07ecc84d5c15fca6f48f3a7e29bb59
etag = 966d76b2bfff876a7805d794d43e7bfdee18fe8ae64b73979da23bb27bac21b7
weak
[file "src/Tests/System/Threading/Tasks/AsyncLazy.cs"]
url = https://github.com/devlooped/catbag/blob/main/System/Threading/Tasks/AsyncLazy.cs
sha = 9f3330f09713aa5f746047e3a50ee839147a5797
etag = 73320600b7a18e0eb25cadc3d687c69dc79181b0458facf526666e150c634782
weak
[file "src/Tests/Microsoft/Extensions/DependencyInjection/AddAsyncLazyExtension.cs"]
url = https://github.com/devlooped/catbag/blob/main/Microsoft/Extensions/DependencyInjection/AddAsyncLazyExtension.cs
sha = 2f8a7d3dffc4409dbda61afb43326ab9d871c1ec
Expand Down
27 changes: 24 additions & 3 deletions docs/_config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# add cert and key for ssl
ssl_cert: localhost.crt
ssl_key: localhost.key
#ssl_cert: localhost.crt
#ssl_key: localhost.key
baseurl: "/SponsorLink"
permalink: pretty

Expand Down Expand Up @@ -111,4 +111,25 @@ issues_template: |
</tr>
{{/each}}
</table>
{{/if}}
{{/if}}
oss_template: |
<p class="mt-6">You contributed to the following repositories:</p>
<table class="borderless" style="border-collapse: collapse; padding: 4px; min-width: unset;">
<tr>
<th class="borderless">Repository</th>
<th class="borderless">Packages (downloads/day)</th>
</tr>
{{#each repositories}}
<tr>
<td class="borderless" style="vertical-align: text-top;"><a href="https://github.com/{{repo}}" target="_blank">{{repo}}</a></td>
<td class="borderless" style="width: 100%; vertical-align: text-top;">
<ul>
{{#each packages}}
<li><a href="https://nuget.org/packages/{{id}}" target="_blank">{{id}}</a> ({{format downloads}})</li>
{{/each}}
</ul>
</td>
</tr>
{{/each}}
</table>
10 changes: 10 additions & 0 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

<html lang="{{ site.lang | default: 'en-US' }}">
{% include head.html %}
<script>
window.site = {
oss_template: `
{{ site.oss_template }}
`,
item_template: `
{{ site.item_template }}
`
};
</script>
<body>
<a class="skip-to-main" href="#main-content">Skip to main content</a>
{% include icons/icons.html %}
Expand Down
73 changes: 73 additions & 0 deletions docs/assets/js/oss.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Handlebars.registerHelper('format', function(number) {
return new Intl.NumberFormat().format(number);
});
var template = Handlebars.compile(window.site.oss_template);

var data = {
authors: { },
repositories: { },
packages: { }
};

fetch('https://raw.githubusercontent.com/devlooped/nuget/refs/heads/main/nuget.json')
.then(response => {
// Check if the response is successful
if (!response.ok) {
setError(`Failed to retrieve OSS data: ${response.status}`);
throw new Error('Failed to retrieve OSS data');
}
return response.json();
})
.then(json => {
data = json;
setBusy(false);
});

async function lookupAccount() {
setBusy(true);
setError('');
document.getElementById('data').innerHTML = '';
var account = document.getElementById('account').value;
console.log('Looking up account: ' + account);

if (account === '') {
setError('Please enter your github account.');
setBusy(false);
return;
}

if (data.authors[account] === undefined) {
document.getElementById('unsupported').style.display = '';
document.getElementById('supported').style.display = 'none';
} else {
const model = {
repositories: data.authors[account].sort().map(repo => ({
repo: repo,
packages: Object.entries(data.packages[repo])
.map(([id, downloads]) => ({
id: id,
downloads: downloads
}))
.sort((a, b) => a.id.localeCompare(b.id))
}))
};
document.getElementById('data').innerHTML = template(model);
document.getElementById('unsupported').style.display = 'none';
document.getElementById('supported').style.display = '';
}

setBusy(false);
}

function setError(message) {
document.getElementById('error').innerHTML = message;
if (message !== '') {
document.getElementById('error').classList.add('warning');
} else {
document.getElementById('error').classList.remove('warning');
}
}

function setBusy(busy) {
document.getElementById('spinner').style.display = busy ? '' : 'none';
}
11 changes: 11 additions & 0 deletions docs/github/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,17 @@ The backend's authentication and configuration can be tested manually by navigat
which would redirect to the GitHub OAuth app for authentication and upon returning to your issuer site, return the
user's profile and claims as JSON.

Optional backend app settings (all prefixed with `SponsorLink:`):

| Setting | Description | Default |
|---------|-------------|---------|
| ManifestBranch | The branch to look for the sponsorable manifest | Default branch retrieved from GitHub API |
| ManifestExpiration | The maximum timespan to cache the sponsorable manifest | `01:00:00` |
| BadgeExpiration | The maximum timespan to cache the usage badge | `00:05:00` |
| LogAnalytics | The Azure Log Analytics workspace ID to produce usage badges | |
| NoContributors | Do not consider code contributors to sponsorable repositories as sponsors | `false` |
| NoOpenSource | Do not consider open source authors to active nuget packages as sponsors | `false` |

## Conclusion

By requiring a GitHub OAuth app for each the sponsorable, the reference implementation avoids having a central
Expand Down
45 changes: 45 additions & 0 deletions docs/github/oss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: OSS Authors
parent: GitHub Sponsors
page_toc: false
---

<div id="spinner" class="spinner text-green-200" role="status"></div>

# OSS Authors

[Devlooped](https://devlooped.com) is a proud supporter of open-source software and its
authors. Therefore, we consider indirect sponsors any and all contributors to active
nuget packages that are open-source and have a GitHub repository.

> An active nuget package has at least 200 downloads per day in aggregate across the last 5 versions.
This page allows you to check your eligibility for indirect sponsorship as an OSS author/contributor.

<div id="github">
<table class="borderless" style="border-collapse: collapse; padding: 4px; min-width: unset;" markdown="0">
<tr>
<td class="borderless">GitHub account:</td>
<td class="borderless">
<input id="account" onblur="lookupAccount();" class="border">
</td>
<td class="borderless">
<div>
<button class="btn btn-green" onclick="lookupAccount();">Go</button>
</div>
</td>
<td class="borderless" style="display: none;" id="unsupported">
⚠️ Account is not eligible as OSS author
</td>
<td class="borderless" style="display: none;" id="supported">
✅ Account is eligible as OSS author
</td>
</tr>
</table>
</div>

<p id="error" class="no-before" />

<div id="data"></div>

<script src="{{ site.baseurl }}/assets/js/oss.js"></script>
2 changes: 1 addition & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Manifest Spec
nav_order: 2
has_children: true
has_toc: false
current: 2.0.0
current: 2.0.1
---

{%- assign versions = site[page.collection]
Expand Down
165 changes: 165 additions & 0 deletions docs/spec/2.0.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
layout: default
title: 2.0.1
parent: Manifest Spec
---
<!-- #content -->
# SponsorLink Version 2.0.1

## Overview

A sponsor manifest is a JWT (JSON Web Token) that encapsulates a user' sponsorship relationship
with another user or organization (the "sponsorable"). This manifest is issued and signed by the
sponsorable (or the sponsorship platform, if supported). There are no third-party intermediaries
between the sponsorable (or sponsorship platform) and the sponsor.

The sponsor manifest can be utilized to enable features exclusive to sponsors or to suppress requests
for sponsorships by the author's code.

During regular usage of a SponsorLink-enabled tool or library, the author might perform an offline
check to verify the user's sponsorship status before enabling a feature or suppressing build warnings.

This check is performed by reading the manifest from a well-known location in the user's environment
and verifying its contents and signature.

Users can subsequently request a manifest to the backend issuer service provided by the author so
that subsequent checks can succeed, as well as renew/sync the manifest if it has expired.

## Purpose

Establishing a standard method to represent and verify sponsorships in a secure, offline, and
privacy-conscious manner would be advantageous for both open-source software (OSS) authors and users.

## Terminology

The term "sponsor" refers to the user or organization that is providing financial support
to another user or organization.

The term "sponsorable" refers to the user or organization that is receiving financial
support from another user or organization.

## Sponsorable Manifest

A sponsorable user or organization can make its support for SponsorLink known by providing
a manifest in JWT (JSON Web Token) format containing the following claims:

| Claim | Description |
| ----------- | ----------- |
| `iss` | [Standard claim](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1) containing the URL of the backend that issues sponsor manifests |
| `aud` | [Standard claim](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) containing one or more URLs of the supported sponsoring platforms |
| `iat` | [Standard claim](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6) containing the time the manifest was issued at |
| `sub_jwk` | [Standard claim](https://openid.net/specs/openid-connect-core-1_0.html#SelfIssuedResponse) containing the public key (JWK) that can be used to check the signature of issued sponsor manifests |
| `schema` | Optional schema version of the manifest. Defaults to 2.0.1 |

This manifest can be discovered automatically by tools that provide sponsor manifest synchronization
and verification.

{: .note }
> By convention, issuers should provide an endpoint at `[iss]/jwt` that returns the sponsorable manifest.
The following is an example of a sponsorable manifest:

```json
{
"iss": "https://sponsorlink.devlooped.com/",
"aud": "https://github.com/sponsors/devlooped",
"iat": 1696118400,
"sub_jwk": {
"e": "AQAB",
"kty": "RSA",
"n": "5inhv8Q..."
}
}
```

## Sponsor Manifest

The sponsor manifest is used to verify the sponsor's sponsorship status. It's a signed JWT
that the sponsorable issuer provides to the sponsor, containing the following claims:

| Claim | Description |
| ----------- | ----------- |
| `iss` | The token [issuer](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1), matching the sponsorable manifest issuer claim |
| `aud` | The [audience](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) URL(s) from the sponsorable manifest |
| `iat` | The [time the manifest was issued at](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6) |
| `sub` | The [subject](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2) claim, which is the sponsor account (i.e. user GitHub login) |
| `roles` | The sponsoring [roles](https://www.rfc-editor.org/rfc/rfc9068.html#section-7.2.1.1) of the authenticated user (e.g. team, org, user, contrib, oss) |
| `email` | The sponsor's email(s) [standard claim](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) |
| `exp` | The token's [expiration date](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4) |
| `schema` | Optional schema version of the manifest. Defaults to 2.0.0 |

{: .note }
> Tools can fetch the sponsorable manifest from `[iss]/jwt` for verification of the sponsor manifest signature.
The [roles](https://www.rfc-editor.org/rfc/rfc9068.html#section-7.2.1.1) claim can be used to distinguish
between different types of sponsorships.

* `user`: The sponsor is personally sponsoring.
* `org`: The user belongs to at least one organization that is sponsoring.
* `contrib`: The user is a contributor and is therefore considered a sponsor.
* `team`: The user is a member of the sponsorable organization or is the sponsorable user.
* `oss`: The user is a contributor to other active open-source projects.

For example, given:

- An organization `acme` that is sponsoring another organization `devlooped`.
- A user `alice` who is a member of the organization `acme`.
- `alice` requests from `devlooped` a sponsor manifest to access a feature.

The issuer provided by `devlooped` would return a signed sponsor manifest token containing the following claims:

```json
{
"iss": "https://sponsorlink.devlooped.com",
"aud": "https://github.com/sponsors/devlooped",
"sub": "alice",
"email": [
"[email protected]",
"[email protected]"
],
"roles": "org",
"exp": 1696118400,
"schema": "2.0.0"
}
```

### Token Signing

The issuing backend provided by the sponsorable signs the sponsor JWT using a private key. The
corresponding public key (available publicly in the sponsorable manifest itself) can be used by
the author's libraries and tools to verify the manifest's signature and expiration date.

## Manifest Usage

Various sponsorship platforms can provide reference implementations of the backend service to issue
SponsorLink manifests using their platform's APIs. For example, GitHub, Open Collective, and Patreon
could all provide such services. Either for self-hosting by authors or as a managed service.

A client-side tool would be provided for users to synchronize their SponsorLink manifest(s) with the
backend service. This tool would typically require the user to authenticate with the platform and
authorize the backend service to access their sponsorship information. This step would be explicit
and require the user's consent to sharing their sponsorship information.

{: .important }
> By convention, sponsor manifests are stored at `~/.sponsorlink/[platform]/[sponsorable].jwt`.
The author's code can check for the manifest presence and verify its contents and signature, which
could enable or disable features, suppress build warnings, or provide other benefits to sponsors.
The author can optionally perform a local-only offline check to ensure the user's email address
in the manifest matches the one in his local source repository configuration.

The author may also choose to verify the token's expiration date, decide on a grace period, and
issue a notification if the manifest is expired.

{: .note }
> See the [GitHub implementation](../github/index.md) of SponsorLink for more information.
## Privacy Considerations

* The method of user identification for manifest generation is entirely determined by the issuer.
* The method of user identification for manifest consumption is left to the discretion of the tool
or library author.

Once a manifest is generated in a user-explicit manner and with their consent, and stored in their
local environment, it can be utilized by any tool or library to locally verify sponsorships without
any further network access.
Loading

0 comments on commit b6d9955

Please sign in to comment.