@oxy.so/core 7.25.0 → 7.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/api/identity.js +19 -2
- package/dist/cjs/crypto/identityDerivations.js +6 -0
- package/dist/cjs/crypto/index.js +6 -3
- package/dist/cjs/index.js +5 -4
- package/dist/cjs/utils/profileLinks.js +34 -0
- package/dist/esm/api/identity.js +19 -2
- package/dist/esm/crypto/identityDerivations.js +6 -0
- package/dist/esm/crypto/index.js +6 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/utils/profileLinks.js +33 -0
- package/dist/types/api/identity.d.ts +18 -1
- package/dist/types/crypto/identityDerivations.d.ts +6 -0
- package/dist/types/crypto/index.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/utils/profileLinks.d.ts +13 -0
- package/package.json +1 -1
- package/src/api/identity.ts +32 -10
- package/src/crypto/identityDerivations.ts +6 -0
- package/src/crypto/index.ts +5 -2
- package/src/index.ts +3 -2
- package/src/utils/profileLinks.ts +44 -0
package/dist/cjs/api/identity.js
CHANGED
|
@@ -118,9 +118,26 @@ class IdentityApi {
|
|
|
118
118
|
* Resolve the W3C DID document for any user. The API derives it on demand
|
|
119
119
|
* from the account's `authMethods` + `publicKey` — there is no stored
|
|
120
120
|
* document. Public (no auth required); short-TTL cached.
|
|
121
|
+
*
|
|
122
|
+
* `{ fresh: true }` reads the document as it is NOW: it skips (and drops)
|
|
123
|
+
* this client's cached copy, is not joined to an in-flight read, and sends
|
|
124
|
+
* `Cache-Control: no-cache` so no HTTP cache on the way answers for the
|
|
125
|
+
* origin. Use it whenever the answer decides where money goes: `#key-1` is
|
|
126
|
+
* the account's Peable receive key and changes on a rotation or an
|
|
127
|
+
* organization key replacement, and a cached copy would derive an address
|
|
128
|
+
* from the previous key for up to the cache TTL.
|
|
121
129
|
*/
|
|
122
|
-
async resolveDid(userId) {
|
|
123
|
-
|
|
130
|
+
async resolveDid(userId, options = {}) {
|
|
131
|
+
const path = `/u/${encodeURIComponent(userId)}/did.json`;
|
|
132
|
+
if (options.fresh) {
|
|
133
|
+
this.ctx.http.invalidateCache({ keys: [`GET:${path}`] });
|
|
134
|
+
return this.ctx.request('GET', path, undefined, {
|
|
135
|
+
cache: false,
|
|
136
|
+
deduplicate: false,
|
|
137
|
+
headers: { 'Cache-Control': 'no-cache' },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return this.ctx.request('GET', path, undefined, {
|
|
124
141
|
cache: true,
|
|
125
142
|
cacheTTL: SHORT_TTL,
|
|
126
143
|
});
|
|
@@ -65,6 +65,12 @@ function deriveScopedSeedFromKey(privateKeyHex, info) {
|
|
|
65
65
|
* `HMAC-SHA256(SOCIAL_RECEIVE_CHAIN_CODE_KEY, compressed identity public key)`.
|
|
66
66
|
* Identical to `@fairco.in/core`'s `deriveSocialReceiveSpendingKey`.
|
|
67
67
|
*
|
|
68
|
+
* INTERNAL to `@oxy.so/core` (not exported from `@oxy.so/core/crypto`): the
|
|
69
|
+
* derivation is non-hardened and the chain code is a public function of the
|
|
70
|
+
* identity public key, so `identity = child - I_L mod n` — whoever holds one
|
|
71
|
+
* child private key and the public key holds the identity key. Callers get
|
|
72
|
+
* signatures ({@link signSocialReceiveDigest}), never the child key.
|
|
73
|
+
*
|
|
68
74
|
* @returns the child private key and its compressed public key, lowercase hex.
|
|
69
75
|
*/
|
|
70
76
|
function deriveSocialReceiveKey(privateKeyHex, index) {
|
package/dist/cjs/crypto/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* common chunk, which is the cost the lazy loading exists to avoid.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.
|
|
15
|
+
exports.verifySignature = exports.signMessage = exports.loadCommonsIdentityBridge = exports.canonicalize = exports.createContentHasher = exports.solveRegistrationPow = exports.deriveIdentityLinkCode = exports.signIdentityProof = exports.digestIdentityPayload = exports.authChallengeDigest = exports.signSocialReceiveDigest = exports.deriveScopedSeedFromKey = exports.MAX_SOCIAL_RECEIVE_INDEX = exports.SOCIAL_RECEIVE_CHAIN_CODE_KEY = exports.SCOPED_SEED_KDF_SALT = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.RecoveryPhraseService = exports.SignatureService = exports.isHealthyOrganizationKeyPair = exports.isOrganizationAccountId = exports.syncOrganizationKeys = exports.deleteOrganizationKey = exports.listOrganizationKeys = exports.getOrganizationRecoveryPhrase = exports.getOrganizationPrivateKey = exports.getOrganizationPublicKey = exports.storeOrganizationKey = exports.organizationKeyFromPhrase = exports.generateOrganizationKey = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = void 0;
|
|
16
16
|
// MUST stay the first import: the shim has to exist before any module below
|
|
17
17
|
// evaluates or draws randomness (see `./polyfill`).
|
|
18
18
|
require("./polyfill");
|
|
@@ -53,13 +53,16 @@ Object.defineProperty(exports, "AEAD_NONCE_LENGTH", { enumerable: true, get: fun
|
|
|
53
53
|
var ecdh_1 = require("./ecdh");
|
|
54
54
|
Object.defineProperty(exports, "deriveSharedSecret", { enumerable: true, get: function () { return ecdh_1.deriveSharedSecret; } });
|
|
55
55
|
// What the identity key computes without leaving its holder: the same functions
|
|
56
|
-
// Commons' identity host implements natively on Android.
|
|
56
|
+
// Commons' identity host implements natively on Android. The social-receive
|
|
57
|
+
// CHILD private key (`deriveSocialReceiveKey`) is deliberately not here: the
|
|
58
|
+
// derivation is non-hardened under a chain code anyone can compute from the
|
|
59
|
+
// public key, so one leaked child private key reveals the identity key. Only
|
|
60
|
+
// signatures (`signSocialReceiveDigest`) leave this module.
|
|
57
61
|
var identityDerivations_1 = require("./identityDerivations");
|
|
58
62
|
Object.defineProperty(exports, "SCOPED_SEED_KDF_SALT", { enumerable: true, get: function () { return identityDerivations_1.SCOPED_SEED_KDF_SALT; } });
|
|
59
63
|
Object.defineProperty(exports, "SOCIAL_RECEIVE_CHAIN_CODE_KEY", { enumerable: true, get: function () { return identityDerivations_1.SOCIAL_RECEIVE_CHAIN_CODE_KEY; } });
|
|
60
64
|
Object.defineProperty(exports, "MAX_SOCIAL_RECEIVE_INDEX", { enumerable: true, get: function () { return identityDerivations_1.MAX_SOCIAL_RECEIVE_INDEX; } });
|
|
61
65
|
Object.defineProperty(exports, "deriveScopedSeedFromKey", { enumerable: true, get: function () { return identityDerivations_1.deriveScopedSeedFromKey; } });
|
|
62
|
-
Object.defineProperty(exports, "deriveSocialReceiveKey", { enumerable: true, get: function () { return identityDerivations_1.deriveSocialReceiveKey; } });
|
|
63
66
|
Object.defineProperty(exports, "signSocialReceiveDigest", { enumerable: true, get: function () { return identityDerivations_1.signSocialReceiveDigest; } });
|
|
64
67
|
Object.defineProperty(exports, "authChallengeDigest", { enumerable: true, get: function () { return identityDerivations_1.authChallengeDigest; } });
|
|
65
68
|
// Identity proofs — the one signed format for operations on a personal root
|
package/dist/cjs/index.js
CHANGED
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
* If a symbol does not appear here, it is NOT part of the public API.
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.
|
|
25
|
-
exports.
|
|
26
|
-
exports.
|
|
27
|
-
exports.packageInfo = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.CENTRAL_IDP_APEX = exports.AUTH_WEB_ORIGIN = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = void 0;
|
|
24
|
+
exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.supportProviderOf = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.commonsDeliveryPlatform = exports.pushTargetsFromDelivery = exports.selectCommonsDelivery = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.OxyAppDataIdentifierError = exports.AccountNotOnDeviceError = exports.REPUTATION_CACHE_PREFIX = exports.StoreInstallerError = exports.setStoreInstaller = exports.getStoreInstaller = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_CATEGORY_IDS = exports.buildUserDid = exports.DirectUploadInterruptedError = exports.MAX_UPLOAD_BYTES = exports.assetUrlCacheTTL = exports.ASSET_MULTIPART_UPLOAD_MAX_BYTES = exports.sameHolderIdentity = exports.retryAfterSecondsOf = exports.ServiceAssetMetadataError = exports.AssetUrlResolutionError = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyApiError = exports.OXY_CLOUD_URL = exports.OXY_API_URL = exports.OxyServices = void 0;
|
|
25
|
+
exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.coerceToSupportedLocale = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = void 0;
|
|
26
|
+
exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.accountNameInvalidKey = exports.accountNameInvalidMessage = exports.accountNameMaxLength = exports.isValidAccountName = exports.isValidDisplayName = exports.isValidPassword = exports.isValidEmail = exports.ACCOUNT_TITLE_INVALID_MESSAGE = exports.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = exports.PASSWORD_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.parseHttpErrorBody = exports.isHttpRequestError = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.trustTierLabel = exports.reputationCategoryLabel = exports.accountRoleLabel = exports.accountCategoryLabel = exports.getLocalesVersion = exports.subscribeLocales = exports.isLocaleLoaded = exports.loadLocale = exports.translate = void 0;
|
|
27
|
+
exports.packageInfo = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.CENTRAL_IDP_APEX = exports.AUTH_WEB_ORIGIN = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = void 0;
|
|
28
28
|
// The crypto shim MUST stay the first import. The root itself ships no crypto
|
|
29
29
|
// library (`__tests__/rootEntryGraph.test.ts`), but `@oxy.so/protocol`'s root
|
|
30
30
|
// entry (reached by the OAuth/PKCE and device helpers) does, and Metro does not
|
|
@@ -97,6 +97,7 @@ Object.defineProperty(exports, "getCanonicalUserHandle", { enumerable: true, get
|
|
|
97
97
|
Object.defineProperty(exports, "getNormalizedUserHandle", { enumerable: true, get: function () { return userHandle_1.getNormalizedUserHandle; } });
|
|
98
98
|
var profileLinks_1 = require("./utils/profileLinks");
|
|
99
99
|
Object.defineProperty(exports, "normalizeProfileLinks", { enumerable: true, get: function () { return profileLinks_1.normalizeProfileLinks; } });
|
|
100
|
+
Object.defineProperty(exports, "supportProviderOf", { enumerable: true, get: function () { return profileLinks_1.supportProviderOf; } });
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
102
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
102
103
|
// ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeProfileLinks = normalizeProfileLinks;
|
|
4
|
+
exports.supportProviderOf = supportProviderOf;
|
|
4
5
|
function cleanUrl(value) {
|
|
5
6
|
if (typeof value !== 'string')
|
|
6
7
|
return null;
|
|
@@ -37,12 +38,14 @@ function normalizeProfileLinks(linksMetadata, links) {
|
|
|
37
38
|
const title = typeof entry?.title === 'string' ? entry.title : undefined;
|
|
38
39
|
const description = typeof entry?.description === 'string' ? entry.description : undefined;
|
|
39
40
|
const image = typeof entry?.image === 'string' ? entry.image : undefined;
|
|
41
|
+
const faviconUrl = typeof entry?.faviconUrl === 'string' ? entry.faviconUrl : undefined;
|
|
40
42
|
result.push({
|
|
41
43
|
id,
|
|
42
44
|
url,
|
|
43
45
|
...(title !== undefined ? { title } : {}),
|
|
44
46
|
...(description !== undefined ? { description } : {}),
|
|
45
47
|
...(image !== undefined ? { image } : {}),
|
|
48
|
+
...(faviconUrl !== undefined ? { faviconUrl } : {}),
|
|
46
49
|
});
|
|
47
50
|
});
|
|
48
51
|
return result;
|
|
@@ -59,3 +62,34 @@ function normalizeProfileLinks(linksMetadata, links) {
|
|
|
59
62
|
}
|
|
60
63
|
return [];
|
|
61
64
|
}
|
|
65
|
+
const SUPPORT_PROVIDERS = [
|
|
66
|
+
{ provider: { id: 'patreon', name: 'Patreon' }, hosts: ['patreon.com'] },
|
|
67
|
+
{ provider: { id: 'kofi', name: 'Ko-fi' }, hosts: ['ko-fi.com'] },
|
|
68
|
+
{ provider: { id: 'buymeacoffee', name: 'Buy Me a Coffee' }, hosts: ['buymeacoffee.com'] },
|
|
69
|
+
{ provider: { id: 'github-sponsors', name: 'GitHub Sponsors' }, hosts: ['github.com'] },
|
|
70
|
+
{ provider: { id: 'liberapay', name: 'Liberapay' }, hosts: ['liberapay.com'] },
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* The support platform a link points at, or `null`. GitHub Sponsors is the one
|
|
74
|
+
* host shared with ordinary GitHub pages, so only its `/sponsors/` path counts.
|
|
75
|
+
*/
|
|
76
|
+
function supportProviderOf(url) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(url);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
85
|
+
for (const { provider, hosts } of SUPPORT_PROVIDERS) {
|
|
86
|
+
if (!hosts.some((known) => host === known || host.endsWith(`.${known}`)))
|
|
87
|
+
continue;
|
|
88
|
+
if (provider.id === 'github-sponsors' &&
|
|
89
|
+
!parsed.pathname.toLowerCase().startsWith('/sponsors/')) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
return provider;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
package/dist/esm/api/identity.js
CHANGED
|
@@ -80,9 +80,26 @@ export class IdentityApi {
|
|
|
80
80
|
* Resolve the W3C DID document for any user. The API derives it on demand
|
|
81
81
|
* from the account's `authMethods` + `publicKey` — there is no stored
|
|
82
82
|
* document. Public (no auth required); short-TTL cached.
|
|
83
|
+
*
|
|
84
|
+
* `{ fresh: true }` reads the document as it is NOW: it skips (and drops)
|
|
85
|
+
* this client's cached copy, is not joined to an in-flight read, and sends
|
|
86
|
+
* `Cache-Control: no-cache` so no HTTP cache on the way answers for the
|
|
87
|
+
* origin. Use it whenever the answer decides where money goes: `#key-1` is
|
|
88
|
+
* the account's Peable receive key and changes on a rotation or an
|
|
89
|
+
* organization key replacement, and a cached copy would derive an address
|
|
90
|
+
* from the previous key for up to the cache TTL.
|
|
83
91
|
*/
|
|
84
|
-
async resolveDid(userId) {
|
|
85
|
-
|
|
92
|
+
async resolveDid(userId, options = {}) {
|
|
93
|
+
const path = `/u/${encodeURIComponent(userId)}/did.json`;
|
|
94
|
+
if (options.fresh) {
|
|
95
|
+
this.ctx.http.invalidateCache({ keys: [`GET:${path}`] });
|
|
96
|
+
return this.ctx.request('GET', path, undefined, {
|
|
97
|
+
cache: false,
|
|
98
|
+
deduplicate: false,
|
|
99
|
+
headers: { 'Cache-Control': 'no-cache' },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return this.ctx.request('GET', path, undefined, {
|
|
86
103
|
cache: true,
|
|
87
104
|
cacheTTL: SHORT_TTL,
|
|
88
105
|
});
|
|
@@ -58,6 +58,12 @@ export function deriveScopedSeedFromKey(privateKeyHex, info) {
|
|
|
58
58
|
* `HMAC-SHA256(SOCIAL_RECEIVE_CHAIN_CODE_KEY, compressed identity public key)`.
|
|
59
59
|
* Identical to `@fairco.in/core`'s `deriveSocialReceiveSpendingKey`.
|
|
60
60
|
*
|
|
61
|
+
* INTERNAL to `@oxy.so/core` (not exported from `@oxy.so/core/crypto`): the
|
|
62
|
+
* derivation is non-hardened and the chain code is a public function of the
|
|
63
|
+
* identity public key, so `identity = child - I_L mod n` — whoever holds one
|
|
64
|
+
* child private key and the public key holds the identity key. Callers get
|
|
65
|
+
* signatures ({@link signSocialReceiveDigest}), never the child key.
|
|
66
|
+
*
|
|
61
67
|
* @returns the child private key and its compressed public key, lowercase hex.
|
|
62
68
|
*/
|
|
63
69
|
export function deriveSocialReceiveKey(privateKeyHex, index) {
|
package/dist/esm/crypto/index.js
CHANGED
|
@@ -24,8 +24,12 @@ export { hkdfSha256 } from './kdf.js';
|
|
|
24
24
|
export { encryptAead, decryptAead, AEAD_KEY_LENGTH, AEAD_NONCE_LENGTH } from './aead.js';
|
|
25
25
|
export { deriveSharedSecret } from './ecdh.js';
|
|
26
26
|
// What the identity key computes without leaving its holder: the same functions
|
|
27
|
-
// Commons' identity host implements natively on Android.
|
|
28
|
-
|
|
27
|
+
// Commons' identity host implements natively on Android. The social-receive
|
|
28
|
+
// CHILD private key (`deriveSocialReceiveKey`) is deliberately not here: the
|
|
29
|
+
// derivation is non-hardened under a chain code anyone can compute from the
|
|
30
|
+
// public key, so one leaked child private key reveals the identity key. Only
|
|
31
|
+
// signatures (`signSocialReceiveDigest`) leave this module.
|
|
32
|
+
export { SCOPED_SEED_KDF_SALT, SOCIAL_RECEIVE_CHAIN_CODE_KEY, MAX_SOCIAL_RECEIVE_INDEX, deriveScopedSeedFromKey, signSocialReceiveDigest, authChallengeDigest, } from './identityDerivations.js';
|
|
29
33
|
// Identity proofs — the one signed format for operations on a personal root
|
|
30
34
|
// (docs/adr/0024-one-oxy-account-root-holders.md D7)
|
|
31
35
|
export { digestIdentityPayload, signIdentityProof } from './identityProof.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -53,7 +53,7 @@ export { selectCommonsDelivery, pushTargetsFromDelivery, commonsDeliveryPlatform
|
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
54
|
export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity.js';
|
|
55
55
|
export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle.js';
|
|
56
|
-
export { normalizeProfileLinks } from './utils/profileLinks.js';
|
|
56
|
+
export { normalizeProfileLinks, supportProviderOf } from './utils/profileLinks.js';
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
@@ -34,12 +34,14 @@ export function normalizeProfileLinks(linksMetadata, links) {
|
|
|
34
34
|
const title = typeof entry?.title === 'string' ? entry.title : undefined;
|
|
35
35
|
const description = typeof entry?.description === 'string' ? entry.description : undefined;
|
|
36
36
|
const image = typeof entry?.image === 'string' ? entry.image : undefined;
|
|
37
|
+
const faviconUrl = typeof entry?.faviconUrl === 'string' ? entry.faviconUrl : undefined;
|
|
37
38
|
result.push({
|
|
38
39
|
id,
|
|
39
40
|
url,
|
|
40
41
|
...(title !== undefined ? { title } : {}),
|
|
41
42
|
...(description !== undefined ? { description } : {}),
|
|
42
43
|
...(image !== undefined ? { image } : {}),
|
|
44
|
+
...(faviconUrl !== undefined ? { faviconUrl } : {}),
|
|
43
45
|
});
|
|
44
46
|
});
|
|
45
47
|
return result;
|
|
@@ -56,3 +58,34 @@ export function normalizeProfileLinks(linksMetadata, links) {
|
|
|
56
58
|
}
|
|
57
59
|
return [];
|
|
58
60
|
}
|
|
61
|
+
const SUPPORT_PROVIDERS = [
|
|
62
|
+
{ provider: { id: 'patreon', name: 'Patreon' }, hosts: ['patreon.com'] },
|
|
63
|
+
{ provider: { id: 'kofi', name: 'Ko-fi' }, hosts: ['ko-fi.com'] },
|
|
64
|
+
{ provider: { id: 'buymeacoffee', name: 'Buy Me a Coffee' }, hosts: ['buymeacoffee.com'] },
|
|
65
|
+
{ provider: { id: 'github-sponsors', name: 'GitHub Sponsors' }, hosts: ['github.com'] },
|
|
66
|
+
{ provider: { id: 'liberapay', name: 'Liberapay' }, hosts: ['liberapay.com'] },
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* The support platform a link points at, or `null`. GitHub Sponsors is the one
|
|
70
|
+
* host shared with ordinary GitHub pages, so only its `/sponsors/` path counts.
|
|
71
|
+
*/
|
|
72
|
+
export function supportProviderOf(url) {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = new URL(url);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
81
|
+
for (const { provider, hosts } of SUPPORT_PROVIDERS) {
|
|
82
|
+
if (!hosts.some((known) => host === known || host.endsWith(`.${known}`)))
|
|
83
|
+
continue;
|
|
84
|
+
if (provider.id === 'github-sponsors' &&
|
|
85
|
+
!parsed.pathname.toLowerCase().startsWith('/sponsors/')) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
return provider;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
@@ -10,6 +10,15 @@ export interface VerifyDomainResult {
|
|
|
10
10
|
export interface RemoveDomainResult {
|
|
11
11
|
success: boolean;
|
|
12
12
|
}
|
|
13
|
+
/** Options for {@link IdentityApi.resolveDid}. */
|
|
14
|
+
export interface ResolveDidOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Read the document from the origin now, never from a cache. Required when
|
|
17
|
+
* `#key-1` decides where money goes (deriving a Peable receive address): the
|
|
18
|
+
* key changes on a rotation or an organization key replacement.
|
|
19
|
+
*/
|
|
20
|
+
fresh?: boolean;
|
|
21
|
+
}
|
|
13
22
|
/** How the caller proves control of the CURRENT key during a key rotation. */
|
|
14
23
|
export type RotateKeyProof = 'device' | 'phrase';
|
|
15
24
|
/** Options for {@link IdentityApi.rotateKey}. */
|
|
@@ -93,8 +102,16 @@ export declare class IdentityApi {
|
|
|
93
102
|
* Resolve the W3C DID document for any user. The API derives it on demand
|
|
94
103
|
* from the account's `authMethods` + `publicKey` — there is no stored
|
|
95
104
|
* document. Public (no auth required); short-TTL cached.
|
|
105
|
+
*
|
|
106
|
+
* `{ fresh: true }` reads the document as it is NOW: it skips (and drops)
|
|
107
|
+
* this client's cached copy, is not joined to an in-flight read, and sends
|
|
108
|
+
* `Cache-Control: no-cache` so no HTTP cache on the way answers for the
|
|
109
|
+
* origin. Use it whenever the answer decides where money goes: `#key-1` is
|
|
110
|
+
* the account's Peable receive key and changes on a rotation or an
|
|
111
|
+
* organization key replacement, and a cached copy would derive an address
|
|
112
|
+
* from the previous key for up to the cache TTL.
|
|
96
113
|
*/
|
|
97
|
-
resolveDid(userId: string): Promise<DidDocument>;
|
|
114
|
+
resolveDid(userId: string, options?: ResolveDidOptions): Promise<DidDocument>;
|
|
98
115
|
/**
|
|
99
116
|
* The signed-in user's linked authentication methods plus their DID. Each
|
|
100
117
|
* `identity` method carries a `verificationMethodId` linking it to its DID
|
|
@@ -37,6 +37,12 @@ export declare function deriveScopedSeedFromKey(privateKeyHex: string, info: str
|
|
|
37
37
|
* `HMAC-SHA256(SOCIAL_RECEIVE_CHAIN_CODE_KEY, compressed identity public key)`.
|
|
38
38
|
* Identical to `@fairco.in/core`'s `deriveSocialReceiveSpendingKey`.
|
|
39
39
|
*
|
|
40
|
+
* INTERNAL to `@oxy.so/core` (not exported from `@oxy.so/core/crypto`): the
|
|
41
|
+
* derivation is non-hardened and the chain code is a public function of the
|
|
42
|
+
* identity public key, so `identity = child - I_L mod n` — whoever holds one
|
|
43
|
+
* child private key and the public key holds the identity key. Callers get
|
|
44
|
+
* signatures ({@link signSocialReceiveDigest}), never the child key.
|
|
45
|
+
*
|
|
40
46
|
* @returns the child private key and its compressed public key, lowercase hex.
|
|
41
47
|
*/
|
|
42
48
|
export declare function deriveSocialReceiveKey(privateKeyHex: string, index: number): {
|
|
@@ -27,7 +27,7 @@ export { hkdfSha256 } from './kdf';
|
|
|
27
27
|
export { encryptAead, decryptAead, AEAD_KEY_LENGTH, AEAD_NONCE_LENGTH } from './aead';
|
|
28
28
|
export type { AeadResult } from './aead';
|
|
29
29
|
export { deriveSharedSecret } from './ecdh';
|
|
30
|
-
export { SCOPED_SEED_KDF_SALT, SOCIAL_RECEIVE_CHAIN_CODE_KEY, MAX_SOCIAL_RECEIVE_INDEX, deriveScopedSeedFromKey,
|
|
30
|
+
export { SCOPED_SEED_KDF_SALT, SOCIAL_RECEIVE_CHAIN_CODE_KEY, MAX_SOCIAL_RECEIVE_INDEX, deriveScopedSeedFromKey, signSocialReceiveDigest, authChallengeDigest, } from './identityDerivations';
|
|
31
31
|
export { digestIdentityPayload, signIdentityProof } from './identityProof';
|
|
32
32
|
export { deriveIdentityLinkCode } from './identityLink';
|
|
33
33
|
export { solveRegistrationPow } from './registrationPow';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export { DirectUploadInterruptedError } from './api/directUpload';
|
|
|
41
41
|
export type { DirectUploadProgress } from './api/directUpload';
|
|
42
42
|
export type { AssetsApi, AssetRecord, UploadedAsset, AssetLinkState, AssetUploadOptions, AssetUploadStageProgress, AssetLinkTarget, AssetDeleteResult, CompletedAssetUpload, LargeUploadOptions, LargeUploadProgress, LargeUploadResult, } from './api/assets';
|
|
43
43
|
export { buildUserDid } from './api/identity';
|
|
44
|
-
export type { IdentityApi, VerifyDomainResult, RemoveDomainResult, RotateKeyProof, RotateKeyOptions, RotateKeyResult, } from './api/identity';
|
|
44
|
+
export type { IdentityApi, VerifyDomainResult, RemoveDomainResult, ResolveDidOptions, RotateKeyProof, RotateKeyOptions, RotateKeyResult, } from './api/identity';
|
|
45
45
|
export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './api/accounts';
|
|
46
46
|
export type { AccountsApi, AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, AccountSuccessResult, SwitchAccountResult, } from './api/accounts';
|
|
47
47
|
export type { AppsApi, AppAssertionsApi, Application, ApplicationCallerMembership, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, RotateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, PublicApplication, ConnectedApp, ConnectedMcpClient, ApplicationDeleteResult, ApplicationFilesWebhook, } from './api/apps';
|
|
@@ -70,8 +70,8 @@ export type { CommonsDeliveryFacts, CommonsDeliveryPlatform, CommonsDeliveryRout
|
|
|
70
70
|
export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity';
|
|
71
71
|
export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle';
|
|
72
72
|
export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHandle';
|
|
73
|
-
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
74
|
-
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
73
|
+
export { normalizeProfileLinks, supportProviderOf } from './utils/profileLinks';
|
|
74
|
+
export type { ProfileLink, ProfileLinkMetadata, SupportProvider } from './utils/profileLinks';
|
|
75
75
|
export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
|
|
76
76
|
export type { HandleApiErrorOptions } from './utils/authHelpers';
|
|
77
77
|
export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
|
|
@@ -13,6 +13,8 @@ export interface ProfileLink {
|
|
|
13
13
|
url: string;
|
|
14
14
|
description?: string;
|
|
15
15
|
image?: string;
|
|
16
|
+
/** Clarity's favicon for the link's host, when Clarity has one. */
|
|
17
|
+
faviconUrl?: string;
|
|
16
18
|
}
|
|
17
19
|
/** Source shape of a single `User.linksMetadata` entry. */
|
|
18
20
|
export interface ProfileLinkMetadata {
|
|
@@ -21,6 +23,7 @@ export interface ProfileLinkMetadata {
|
|
|
21
23
|
description?: string;
|
|
22
24
|
image?: string;
|
|
23
25
|
id?: string;
|
|
26
|
+
faviconUrl?: string;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* Normalizes a user's profile links into a clean display shape.
|
|
@@ -42,3 +45,13 @@ export interface ProfileLinkMetadata {
|
|
|
42
45
|
* as `https://`; prefixing is a display concern left to the caller.
|
|
43
46
|
*/
|
|
44
47
|
export declare function normalizeProfileLinks(linksMetadata?: ProfileLinkMetadata[], links?: string[]): ProfileLink[];
|
|
48
|
+
/** A platform a profile can point supporters to, recognised by the link's host. */
|
|
49
|
+
export interface SupportProvider {
|
|
50
|
+
id: 'patreon' | 'kofi' | 'buymeacoffee' | 'github-sponsors' | 'liberapay';
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The support platform a link points at, or `null`. GitHub Sponsors is the one
|
|
55
|
+
* host shared with ordinary GitHub pages, so only its `/sponsors/` path counts.
|
|
56
|
+
*/
|
|
57
|
+
export declare function supportProviderOf(url: string): SupportProvider | null;
|
package/package.json
CHANGED
package/src/api/identity.ts
CHANGED
|
@@ -68,6 +68,16 @@ export interface RemoveDomainResult {
|
|
|
68
68
|
success: boolean;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Options for {@link IdentityApi.resolveDid}. */
|
|
72
|
+
export interface ResolveDidOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Read the document from the origin now, never from a cache. Required when
|
|
75
|
+
* `#key-1` decides where money goes (deriving a Peable receive address): the
|
|
76
|
+
* key changes on a rotation or an organization key replacement.
|
|
77
|
+
*/
|
|
78
|
+
fresh?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
/** How the caller proves control of the CURRENT key during a key rotation. */
|
|
72
82
|
export type RotateKeyProof = 'device' | 'phrase';
|
|
73
83
|
|
|
@@ -191,17 +201,29 @@ export class IdentityApi {
|
|
|
191
201
|
* Resolve the W3C DID document for any user. The API derives it on demand
|
|
192
202
|
* from the account's `authMethods` + `publicKey` — there is no stored
|
|
193
203
|
* document. Public (no auth required); short-TTL cached.
|
|
204
|
+
*
|
|
205
|
+
* `{ fresh: true }` reads the document as it is NOW: it skips (and drops)
|
|
206
|
+
* this client's cached copy, is not joined to an in-flight read, and sends
|
|
207
|
+
* `Cache-Control: no-cache` so no HTTP cache on the way answers for the
|
|
208
|
+
* origin. Use it whenever the answer decides where money goes: `#key-1` is
|
|
209
|
+
* the account's Peable receive key and changes on a rotation or an
|
|
210
|
+
* organization key replacement, and a cached copy would derive an address
|
|
211
|
+
* from the previous key for up to the cache TTL.
|
|
194
212
|
*/
|
|
195
|
-
async resolveDid(userId: string): Promise<DidDocument> {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
undefined,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
213
|
+
async resolveDid(userId: string, options: ResolveDidOptions = {}): Promise<DidDocument> {
|
|
214
|
+
const path = `/u/${encodeURIComponent(userId)}/did.json`;
|
|
215
|
+
if (options.fresh) {
|
|
216
|
+
this.ctx.http.invalidateCache({ keys: [`GET:${path}`] });
|
|
217
|
+
return this.ctx.request<DidDocument>('GET', path, undefined, {
|
|
218
|
+
cache: false,
|
|
219
|
+
deduplicate: false,
|
|
220
|
+
headers: { 'Cache-Control': 'no-cache' },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return this.ctx.request<DidDocument>('GET', path, undefined, {
|
|
224
|
+
cache: true,
|
|
225
|
+
cacheTTL: SHORT_TTL,
|
|
226
|
+
});
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
/**
|
|
@@ -73,6 +73,12 @@ export function deriveScopedSeedFromKey(privateKeyHex: string, info: string): Ui
|
|
|
73
73
|
* `HMAC-SHA256(SOCIAL_RECEIVE_CHAIN_CODE_KEY, compressed identity public key)`.
|
|
74
74
|
* Identical to `@fairco.in/core`'s `deriveSocialReceiveSpendingKey`.
|
|
75
75
|
*
|
|
76
|
+
* INTERNAL to `@oxy.so/core` (not exported from `@oxy.so/core/crypto`): the
|
|
77
|
+
* derivation is non-hardened and the chain code is a public function of the
|
|
78
|
+
* identity public key, so `identity = child - I_L mod n` — whoever holds one
|
|
79
|
+
* child private key and the public key holds the identity key. Callers get
|
|
80
|
+
* signatures ({@link signSocialReceiveDigest}), never the child key.
|
|
81
|
+
*
|
|
76
82
|
* @returns the child private key and its compressed public key, lowercase hex.
|
|
77
83
|
*/
|
|
78
84
|
export function deriveSocialReceiveKey(
|
package/src/crypto/index.ts
CHANGED
|
@@ -57,13 +57,16 @@ export type { AeadResult } from './aead';
|
|
|
57
57
|
export { deriveSharedSecret } from './ecdh';
|
|
58
58
|
|
|
59
59
|
// What the identity key computes without leaving its holder: the same functions
|
|
60
|
-
// Commons' identity host implements natively on Android.
|
|
60
|
+
// Commons' identity host implements natively on Android. The social-receive
|
|
61
|
+
// CHILD private key (`deriveSocialReceiveKey`) is deliberately not here: the
|
|
62
|
+
// derivation is non-hardened under a chain code anyone can compute from the
|
|
63
|
+
// public key, so one leaked child private key reveals the identity key. Only
|
|
64
|
+
// signatures (`signSocialReceiveDigest`) leave this module.
|
|
61
65
|
export {
|
|
62
66
|
SCOPED_SEED_KDF_SALT,
|
|
63
67
|
SOCIAL_RECEIVE_CHAIN_CODE_KEY,
|
|
64
68
|
MAX_SOCIAL_RECEIVE_INDEX,
|
|
65
69
|
deriveScopedSeedFromKey,
|
|
66
|
-
deriveSocialReceiveKey,
|
|
67
70
|
signSocialReceiveDigest,
|
|
68
71
|
authChallengeDigest,
|
|
69
72
|
} from './identityDerivations';
|
package/src/index.ts
CHANGED
|
@@ -133,6 +133,7 @@ export type {
|
|
|
133
133
|
IdentityApi,
|
|
134
134
|
VerifyDomainResult,
|
|
135
135
|
RemoveDomainResult,
|
|
136
|
+
ResolveDidOptions,
|
|
136
137
|
RotateKeyProof,
|
|
137
138
|
RotateKeyOptions,
|
|
138
139
|
RotateKeyResult,
|
|
@@ -319,8 +320,8 @@ export {
|
|
|
319
320
|
getNormalizedUserHandle,
|
|
320
321
|
} from './utils/userHandle';
|
|
321
322
|
export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHandle';
|
|
322
|
-
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
323
|
-
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
323
|
+
export { normalizeProfileLinks, supportProviderOf } from './utils/profileLinks';
|
|
324
|
+
export type { ProfileLink, ProfileLinkMetadata, SupportProvider } from './utils/profileLinks';
|
|
324
325
|
|
|
325
326
|
// ---------------------------------------------------------------------------
|
|
326
327
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
@@ -13,6 +13,8 @@ export interface ProfileLink {
|
|
|
13
13
|
url: string;
|
|
14
14
|
description?: string;
|
|
15
15
|
image?: string;
|
|
16
|
+
/** Clarity's favicon for the link's host, when Clarity has one. */
|
|
17
|
+
faviconUrl?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** Source shape of a single `User.linksMetadata` entry. */
|
|
@@ -22,6 +24,7 @@ export interface ProfileLinkMetadata {
|
|
|
22
24
|
description?: string;
|
|
23
25
|
image?: string;
|
|
24
26
|
id?: string;
|
|
27
|
+
faviconUrl?: string;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
function cleanUrl(value: unknown): string | null {
|
|
@@ -63,12 +66,14 @@ export function normalizeProfileLinks(
|
|
|
63
66
|
const title = typeof entry?.title === 'string' ? entry.title : undefined;
|
|
64
67
|
const description = typeof entry?.description === 'string' ? entry.description : undefined;
|
|
65
68
|
const image = typeof entry?.image === 'string' ? entry.image : undefined;
|
|
69
|
+
const faviconUrl = typeof entry?.faviconUrl === 'string' ? entry.faviconUrl : undefined;
|
|
66
70
|
result.push({
|
|
67
71
|
id,
|
|
68
72
|
url,
|
|
69
73
|
...(title !== undefined ? { title } : {}),
|
|
70
74
|
...(description !== undefined ? { description } : {}),
|
|
71
75
|
...(image !== undefined ? { image } : {}),
|
|
76
|
+
...(faviconUrl !== undefined ? { faviconUrl } : {}),
|
|
72
77
|
});
|
|
73
78
|
});
|
|
74
79
|
return result;
|
|
@@ -86,3 +91,42 @@ export function normalizeProfileLinks(
|
|
|
86
91
|
|
|
87
92
|
return [];
|
|
88
93
|
}
|
|
94
|
+
|
|
95
|
+
/** A platform a profile can point supporters to, recognised by the link's host. */
|
|
96
|
+
export interface SupportProvider {
|
|
97
|
+
id: 'patreon' | 'kofi' | 'buymeacoffee' | 'github-sponsors' | 'liberapay';
|
|
98
|
+
name: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const SUPPORT_PROVIDERS: readonly { provider: SupportProvider; hosts: readonly string[] }[] = [
|
|
102
|
+
{ provider: { id: 'patreon', name: 'Patreon' }, hosts: ['patreon.com'] },
|
|
103
|
+
{ provider: { id: 'kofi', name: 'Ko-fi' }, hosts: ['ko-fi.com'] },
|
|
104
|
+
{ provider: { id: 'buymeacoffee', name: 'Buy Me a Coffee' }, hosts: ['buymeacoffee.com'] },
|
|
105
|
+
{ provider: { id: 'github-sponsors', name: 'GitHub Sponsors' }, hosts: ['github.com'] },
|
|
106
|
+
{ provider: { id: 'liberapay', name: 'Liberapay' }, hosts: ['liberapay.com'] },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The support platform a link points at, or `null`. GitHub Sponsors is the one
|
|
111
|
+
* host shared with ordinary GitHub pages, so only its `/sponsors/` path counts.
|
|
112
|
+
*/
|
|
113
|
+
export function supportProviderOf(url: string): SupportProvider | null {
|
|
114
|
+
let parsed: URL;
|
|
115
|
+
try {
|
|
116
|
+
parsed = new URL(url);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
121
|
+
for (const { provider, hosts } of SUPPORT_PROVIDERS) {
|
|
122
|
+
if (!hosts.some((known) => host === known || host.endsWith(`.${known}`))) continue;
|
|
123
|
+
if (
|
|
124
|
+
provider.id === 'github-sponsors' &&
|
|
125
|
+
!parsed.pathname.toLowerCase().startsWith('/sponsors/')
|
|
126
|
+
) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
return provider;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|