feat(route/rss3): RSS3 metadata parser (#16486)

* init

* done

Signed-off-by: Innei <i@innei.in>

* chore: cleanup

Signed-off-by: Innei <i@innei.in>

* fix: format

Signed-off-by: Innei <i@innei.in>

* fix: pass through

Signed-off-by: Innei <i@innei.in>

* chore: add testcase

Signed-off-by: Innei <i@innei.in>

* fix: update

Signed-off-by: Innei <i@innei.in>

---------

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-21 22:49:59 +08:00 committed by GitHub
parent 6ef47acf93
commit 2d82ddcddc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 714 additions and 14 deletions

View File

@ -1,6 +1,10 @@
import { Route } from '@/types';
/* eslint-disable default-case */
import { Route, type DataItem } from '@/types';
import { camelcaseKeys } from '@/utils/camelcase-keys';
import ofetch from '@/utils/ofetch';
import type { Action } from '@rss3/sdk';
import type { GetRSS3DataMetadata } from './interfaces/metadata';
export const route: Route = {
path: '/:account/:network?/:tag?',
@ -132,17 +136,447 @@ async function handler(ctx) {
return {
title: `${account} activities`,
link: 'https://rss3.io',
item: data.map((item) => ({
title: `New ${item.tag} ${item.type} action on ${item.network}`,
description: `New ${item.tag} ${item.type} action on ${item.network}<br /><br />From: ${item.from}<br/>To: ${item.to}`,
link: item.actions?.[0]?.related_urls?.[0],
guid: item.id,
author: [
{
name: item.owner,
avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`,
},
],
})),
item: data.map((item) => {
const content = parseItemActionToContent(camelcaseKeys(item.actions));
const description = `New ${item.tag} ${item.type} action on ${item.network}<br /><br />From: ${item.from}<br/>To: ${item.to}`;
return {
title: `New ${item.tag} ${item.type} action on ${item.network}`,
description: content ? `${description}<br /><br />${content}` : description,
link: item.actions?.[0]?.related_urls?.[0],
guid: item.id,
author: [
{
name: item.owner,
avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`,
},
],
_extra: { raw: item },
} as DataItem;
}),
};
}
function parseItemActionToContent(actions: Action[]): string | undefined {
if (!actions) {
return;
}
let joint = '';
for (const action of actions) {
const metadata = action.metadata;
if (!metadata) {
continue;
}
const { tag } = action;
switch (tag) {
case 'social':
joint += renderSocialTagContent(action);
break;
case 'collectible':
joint += renderCollectibleTagContent(action);
break;
case 'metaverse':
joint += renderMetaverseTagContent(action);
break;
case 'exchange':
joint += renderExchange(action);
break;
case 'transaction':
joint += renderTransaction(action);
break;
}
joint += '<hr />';
}
return joint;
}
const renderTransaction = (action: Action) => {
let joint = '';
const { type } = action;
const tag = 'transaction';
switch (type) {
case 'transfer':
case 'burn':
case 'mint':
case 'approval': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Transaction ${type.toUpperCase().at(0) + type.slice(1)}</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p><strong>Standard:</strong> ${metadata.standard}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Decimals:</strong> ${metadata.decimals}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
]);
break;
}
case 'event': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([/* html */ `<h4>Transaction Event</h4>`, /* html */ `<p><strong>Block Hash:</strong> ${metadata.block.hash}</p>`, /* html */ `<p><strong>Transaction Hash:</strong> ${metadata.transaction.hash}</p>`]);
break;
}
case 'bridge': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Transaction Bridge</h4>`,
/* html */ `<p><strong>Action:</strong> ${metadata.action}</p>`,
/* html */ `<p><strong>Source Network:</strong> ${metadata.sourceNetwork}</p>`,
/* html */ `<p><strong>Target Network:</strong> ${metadata.targetNetwork}</p>`,
metadata.token && /* html */ `<p><strong>Token name:</strong> ${metadata.token.name}</p>`,
metadata.token && /* html */ `<p><strong>Token Symbol:</strong> ${metadata.token.symbol}</p>`,
metadata.token && /* html */ `<p><strong>Token Value:</strong> ${metadata.token.value}</p>`,
metadata.token && /* html */ `<p><strong>Token Address:</strong> ${metadata.token.address}</p>`,
]);
break;
}
}
return buildSectionFooterHTML(joint, action);
};
const renderExchange = (action: Action) => {
let joint = '';
const { type } = action;
const tag = 'exchange';
switch (type) {
case 'liquidity': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Exchange Liquidity</h4>`,
/* html */ `<p><strong>Action:</strong> ${metadata.action}</p>`,
/* html */ `<p>
<table>
<thead>
<tr>
<th>Address</th>
<th>Value</th>
<th>Name</th>
<th>Symbol</th>
<th>Decimals</th>
<th>Standard</th>
</tr>
</thead>
<tbody>
${metadata.tokens.map(
(token) => /* html */ `<tr>
<td>${token.address}</td>
<td>${token.value}</td>
<td>${token.name}</td>
<td>${token.symbol}</td>
<td>${token.decimals}</td>
<td>${token.standard}</td>
</tr>`
)}
</tbody>
</table>
</p>`,
]);
break;
}
case 'staking': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Exchange Liquidity</h4>`,
/* html */ `<p><strong>Action:</strong> ${metadata.action}</p>`,
metadata.token &&
/* html */ `<p>
<strong>Token:</strong>
<ul>
<li><strong>Address:</strong> ${metadata.token.address}</li>
<li><strong>Value:</strong> ${metadata.token.value}</li>
<li><strong>Name:</strong> ${metadata.token.name}</li>
<li><strong>Symbol:</strong> ${metadata.token.symbol}</li>
<li><strong>Decimals:</strong> ${metadata.token.decimals}</li>
<li><strong>Standard:</strong> ${metadata.token.standard}</li>
</ul>
</p>`,
]);
break;
}
case 'swap': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Exchange Swap</h4>`,
/* html */ metadata.from && `<p><strong>From:</strong> ${metadata.from.address}</p>`,
/* html */ metadata.to && `<p><strong>To:</strong> ${metadata.to?.address}</p>`,
]);
}
}
return buildSectionFooterHTML(joint, action);
};
const renderMetaverseTagContent = (action: Action) => {
let joint = '';
const { from, to, type } = action;
const tag = 'metaverse';
switch (type) {
case 'burn': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Metaverse Burn</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
break;
}
case 'trade': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Metaverse Trade</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
break;
}
case 'mint':
{
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Metaverse Mint</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
}
break;
case 'transfer': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Metaverse Transfer</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
}
}
return buildSectionFooterHTML(joint, action);
};
const renderCollectibleTagContent = (action: Action) => {
let joint = '';
const { from, to, type } = action;
const tag = 'collectible';
switch (type) {
case 'approval': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Collectible Approval</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
break;
}
case 'burn': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Collectible Burn</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
break;
}
case 'trade': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Collectible Trade</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
break;
}
case 'mint':
{
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Collectible Mint</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
}
break;
case 'transfer': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html */ `<h4>Collectible Transfer</h4>`,
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
/* html */ `<p>${from} --> ${to}</p>`,
]);
}
}
return buildSectionFooterHTML(joint, action);
};
const renderSocialTagContent = (action: Action) => {
let joint = '';
const { from, to, platform, type } = action;
const tag = 'social';
switch (type) {
case 'profile': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
`<p><strong>Name:</strong> ${metadata.name}</p>`,
`<p><strong>Handle:</strong> ${metadata.handle}</p>`,
`<p><strong>Bio:</strong> ${metadata.bio}</p>`,
`<p><strong>Platform:</strong> ${platform}</p>`,
metadata.imageUri && `<img src="https://ipfs.io/ipfs/${metadata.imageUri.split('://')[1]}" alt="${metadata.name}" style="max-width:100%; height:auto;"/>`,
]);
break;
}
case 'mint': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([`<h4>Social Mint</h4>`, `<p><strong>Title:</strong> ${metadata.title}</p>`, `<p>${from} --> ${to}</p>`]);
break;
}
case 'delete': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([`<h4>Social Delete</h4>`, `<p><strong>Title:</strong> ${metadata.title}</p>`]);
break;
}
case 'post': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html*/ `<h4>Social Post</h4>`,
/* html*/ `<p><strong>Title:</strong> ${metadata.title}</p>`,
/* html*/ `<p><strong>Author:</strong> ${metadata.handle}</p>`,
/* html*/ `<p><strong>Content:</strong> <pre>${metadata.body}</pre></p>`,
/* html*/ `<p><strong>Platform:</strong> ${platform}</p>`,
]);
break;
}
case 'comment': {
const metadata = extractMetadata(tag, type, action);
if (!metadata) {
break;
}
joint += buildHTML([
/* html*/ `<h4>Social Comment</h4>`,
/* html*/ `<p><strong>Comment Anchor:</strong><a href="${metadata.authorUrl}" target="_blank">${metadata.handle}</a></p>`,
metadata.target && /* html*/ `<p><strong>Comment Target:</strong> <a href="${metadata.targetUrl}" target="_blank">${metadata.target.title || metadata.targetUrl}</a></p>`,
]);
break;
}
case 'reward':
case 'revise':
case 'proxy':
case 'share':
break;
}
return joint;
};
function extractMetadata<T1 extends string, T2 extends string>(_tag: T1, _type: T2, data: any): GetRSS3DataMetadata<T1, T2> | null {
const metadata = data.metadata;
if (!metadata) {
return null;
}
return camelcaseKeys(data.metadata) as GetRSS3DataMetadata<T1, T2>;
}
function buildHTML(arr: (string | boolean | undefined | null)[]): string {
return arr.filter(Boolean).join('\n');
}
const buildSectionFooterHTML = (string: string, action: Action) =>
buildHTML([
string,
!!action.platform && `<p><strong>Platform:</strong> ${action.platform}</p>`,
/* html */ `<p><strong>Related URLs:</strong>
<ul><li>${action.relatedUrls.map((url) => `<a href="${url}" target="_blank">${url}</a>`).join('</li><li>')}</li></ul></p>`,
]);

View File

@ -0,0 +1,67 @@
import {
CollectibleApproval,
CollectibleBurn,
CollectibleMint,
CollectibleTrade,
CollectibleTransfer,
ExchangeLiquidity,
ExchangeStaking,
ExchangeSwap,
MetaverseBurn,
MetaverseMint,
MetaverseTrade,
MetaverseTransfer,
SocialComment,
SocialDelete,
SocialMint,
SocialPost,
SocialProfile,
SocialProxy,
SocialRevise,
SocialReward,
SocialShare,
StakeStaking,
StakeTransaction,
StakerProfitSnapshot,
TransactionApproval,
TransactionBridge,
TransactionBurn,
TransactionEvent,
TransactionMint,
TransactionTransfer,
} from '@rss3/sdk';
export type RSS3DataModels = {
CollectibleApproval: CollectibleApproval;
CollectibleBurn: CollectibleBurn;
CollectibleMint: CollectibleMint;
CollectibleTrade: CollectibleTrade;
CollectibleTransfer: CollectibleTransfer;
MetaverseBurn: MetaverseBurn;
MetaverseMint: MetaverseMint;
MetaverseTrade: MetaverseTrade;
MetaverseTransfer: MetaverseTransfer;
SocialComment: SocialComment;
SocialDelete: SocialDelete;
SocialMint: SocialMint;
SocialPost: SocialPost;
SocialProfile: SocialProfile;
SocialProxy: SocialProxy;
SocialRevise: SocialRevise;
SocialReward: SocialReward;
SocialShare: SocialShare;
StakeStaking: StakeStaking;
StakeTransaction: StakeTransaction;
StakerProfitSnapshot: StakerProfitSnapshot;
TransactionApproval: TransactionApproval;
TransactionBridge: TransactionBridge;
TransactionBurn: TransactionBurn;
TransactionEvent: TransactionEvent;
TransactionMint: TransactionMint;
TransactionTransfer: TransactionTransfer;
ExchangeLiquidity: ExchangeLiquidity;
ExchangeStaking: ExchangeStaking;
ExchangeSwap: ExchangeSwap;
};
export type GetRSS3DataMetadata<FirstKey extends string, SecondKey extends string> = `${Capitalize<FirstKey>}${Capitalize<SecondKey>}` extends keyof RSS3DataModels
? RSS3DataModels[`${Capitalize<FirstKey>}${Capitalize<SecondKey>}`]
: null;

View File

@ -0,0 +1,127 @@
import { describe, expect, it } from 'vitest';
import { camelcase, camelcaseKeys } from './camelcase-keys';
describe('test camelcase keys', () => {
it('case 1 normal', () => {
const obj = {
tool: 'too',
tool_name: 'too_name',
a_b: 1,
a: 1,
b: {
c_d: 1,
},
};
expect(camelcaseKeys(obj)).toStrictEqual({
tool: 'too',
toolName: 'too_name',
aB: 1,
a: 1,
b: {
cD: 1,
},
});
});
it('case 2: key has number', () => {
const obj = {
b147da0eaecbea00aeb62055: {
data: {},
},
a_c11ab_Ac: [
{
a_b: 1,
},
1,
],
};
expect(camelcaseKeys(obj)).toStrictEqual({
b147da0eaecbea00aeb62055: {
data: {},
},
aC11abAc: [
{
aB: 1,
},
1,
],
});
});
it('case 3: not a object', () => {
const value = 1;
expect(camelcaseKeys(value)).toBe(value);
});
it('case 4: nullable value', () => {
let value = null as any;
expect(camelcaseKeys(value)).toBe(value);
value = undefined;
expect(camelcaseKeys(value)).toBe(value);
value = Number.NaN;
expect(camelcaseKeys(value)).toBe(value);
});
it('case 5: array', () => {
const arr = [
{
a_b: 1,
},
null,
undefined,
+0,
-0,
Number.POSITIVE_INFINITY,
{
a_b: 1,
},
];
expect(camelcaseKeys(arr)).toStrictEqual([
{
aB: 1,
},
null,
undefined,
+0,
-0,
Number.POSITIVE_INFINITY,
{
aB: 1,
},
]);
});
it('case 6: filter out mongo id', () => {
const obj = {
_id: '123',
a_b: 1,
collections: {
posts: {
'661bb93307d35005ba96731b': {},
},
},
};
expect(camelcaseKeys(obj)).toStrictEqual({
id: '123',
aB: 1,
collections: {
posts: {
'661bb93307d35005ba96731b': {},
},
},
});
});
it('case 7: start with underscore should not camelcase', () => {
expect(camelcase('_id')).toBe('id');
});
});

View File

@ -0,0 +1,27 @@
const isObject = (obj: any) => obj && typeof obj === 'object';
const isPlainObject = (obj: any) => isObject(obj) && Object.prototype.toString.call(obj) === '[object Object]' && Object.getPrototypeOf(obj) === Object.prototype;
/**
* A simple camelCase function that only handles strings, but not handling symbol, date, or other complex case.
* If you need to handle more complex cases, please use camelcase-keys package.
*/
export const camelcaseKeys = <T = any>(obj: any): T => {
if (Array.isArray(obj)) {
return obj.map((x) => camelcaseKeys(x)) as any;
}
if (isPlainObject(obj)) {
return Object.keys(obj).reduce((result: any, key) => {
const nextKey = isMongoId(key) ? key : camelcase(key);
result[nextKey] = camelcaseKeys(obj[key]);
return result;
}, {}) as any;
}
return obj;
};
export function camelcase(str: string) {
return str.replace(/^_+/, '').replaceAll(/([_-][a-z])/gi, ($1) => $1.toUpperCase().replace('-', '').replace('_', ''));
}
const isMongoId = (id: string) => id.length === 24 && /^[\dA-Fa-f]{24}$/.test(id);

View File

@ -61,6 +61,7 @@
"@opentelemetry/sdk-trace-base": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"@postlight/parser": "2.2.3",
"@rss3/sdk": "0.0.13",
"@scalar/hono-api-reference": "0.5.141",
"@sentry/node": "7.116.0",
"@tonyrl/rand-user-agent": "2.0.74",
@ -196,4 +197,4 @@
"engines": {
"node": ">=22"
}
}
}

View File

@ -41,6 +41,9 @@ importers:
'@postlight/parser':
specifier: 2.2.3
version: 2.2.3
'@rss3/sdk':
specifier: 0.0.13
version: 0.0.13
'@scalar/hono-api-reference':
specifier: 0.5.141
version: 0.5.141(hono@4.5.7)
@ -1740,6 +1743,15 @@ packages:
cpu: [x64]
os: [win32]
'@rss3/api-core@0.0.13':
resolution: {integrity: sha512-5CRVS0FpCZ8YiS/ttHM0ZxPxM05+WkcEPE7a5klX9NvULbDCISdpYQkiUkdVjZw+hPD7MCtlvBc9tFqTQKojJw==}
'@rss3/api-utils@0.0.13':
resolution: {integrity: sha512-ucN9myX/E0DlA3Q+KkpCD/HwpB6fQSMlaHqRhDDV3tuyYrQnP/PjRKpGibx1Sz2x519SJNnsfyDL931H00iDJA==}
'@rss3/sdk@0.0.13':
resolution: {integrity: sha512-5QcC9NeMiKeay/H1JEGlG2ZtmKhZxlyLTA5f6dcIdGZUrSNk4DCM/J+8u0ED73bMQupi6xnUm+dIJdn6A1kHyA==}
'@scalar/hono-api-reference@0.5.141':
resolution: {integrity: sha512-EFGgjf91RRRlyOJgZfCH/aiD8UXhgIeFdNEdHQZF20UoJr5NvugKUsK3YYm8StkJ3Q7ApXm9ASnkeU2oiGgbnA==}
engines: {node: '>=18'}
@ -4445,6 +4457,12 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
openapi-fetch@0.11.1:
resolution: {integrity: sha512-WtDQsrvxjXuCmo6u6WMQPfUaya8cLfL+ZCaXorPo9MMumqlU/Km/SrCXsEcJH234D4iykOkvJ6Q/iWBzK7+3rA==}
openapi-typescript-helpers@0.0.12:
resolution: {integrity: sha512-FO+5kTWO6KDutigamr2MRwciYkAUYhqdctlyVRrQOe2uxif2/O2+GcS07jNnP36AUK6ubSsGu3GeBiYIc6eQzA==}
openapi3-ts@4.3.3:
resolution: {integrity: sha512-LKkzBGJcZ6wdvkKGMoSvpK+0cbN5Xc3XuYkJskO+vjEQWJgs1kgtyUk0pjf8KwPuysv323Er62F5P17XQl96Qg==}
@ -5408,6 +5426,9 @@ packages:
peerDependencies:
typescript: '>=4.2.0'
ts-case-convert@2.0.7:
resolution: {integrity: sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==}
ts-custom-error@2.2.2:
resolution: {integrity: sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==}
engines: {node: '>=8.0.0'}
@ -7303,6 +7324,21 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.21.0':
optional: true
'@rss3/api-core@0.0.13':
dependencies:
openapi-fetch: 0.11.1
ts-case-convert: 2.0.7
type-fest: 4.25.0
'@rss3/api-utils@0.0.13':
dependencies:
'@rss3/api-core': 0.0.13
'@rss3/sdk@0.0.13':
dependencies:
'@rss3/api-core': 0.0.13
'@rss3/api-utils': 0.0.13
'@scalar/hono-api-reference@0.5.141(hono@4.5.7)':
dependencies:
'@scalar/types': 0.0.3
@ -10402,6 +10438,12 @@ snapshots:
dependencies:
mimic-function: 5.0.1
openapi-fetch@0.11.1:
dependencies:
openapi-typescript-helpers: 0.0.12
openapi-typescript-helpers@0.0.12: {}
openapi3-ts@4.3.3:
dependencies:
yaml: 2.5.0
@ -11443,6 +11485,8 @@ snapshots:
dependencies:
typescript: 5.5.4
ts-case-convert@2.0.7: {}
ts-custom-error@2.2.2: {}
ts-custom-error@3.3.1: {}