@igstack/app-catalog-backend-core 0.3.1-alpha-20260404005709 → 0.3.1-alpha-20260405015231
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/index.d.mts +2 -2
- package/dist/modules/appCatalog/checkLinks.d.mts.map +1 -1
- package/dist/modules/appCatalog/checkLinks.mjs +31 -2
- package/dist/modules/appCatalog/checkLinks.mjs.map +1 -1
- package/dist/types/common/approvalMethodTypes.d.mts +39 -15
- package/dist/types/common/approvalMethodTypes.d.mts.map +1 -1
- package/package.json +3 -3
- package/src/modules/appCatalog/checkLinks.ts +58 -5
- package/src/types/common/approvalMethodTypes.ts +38 -22
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AcMetaDictionary, Tag } from "./types/common/sharedTypes.mjs";
|
|
2
2
|
import { AcResourceIndexed } from "./types/common/resourceTypes.mjs";
|
|
3
|
-
import { AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl,
|
|
3
|
+
import { AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, ServiceConfig } from "./types/common/approvalMethodTypes.mjs";
|
|
4
4
|
import { Group, Person } from "./types/common/personGroupTypes.mjs";
|
|
5
5
|
import { SubResource } from "./types/common/subResourceTypes.mjs";
|
|
6
6
|
import { AppApprovalMethod, AppCatalogData, AppCategory, AppForCatalog, AppTierVariant, AppVersionInfo, GroupingTagDefinition, GroupingTagValue, SourceReference, VersionInfo } from "./types/common/appCatalogTypes.mjs";
|
|
@@ -35,4 +35,4 @@ import { injectCustomScripts } from "./middleware/htmlInjection.mjs";
|
|
|
35
35
|
import { runLighthouseKeeperDemo } from "./modules/lighthouseKeeper/demo.mjs";
|
|
36
36
|
import { APP_CATALOG_AI_SYSTEM_PROMPT, createAppCatalogAITools } from "./modules/lighthouseKeeper/tools.mjs";
|
|
37
37
|
import { getBuildPipelineId, getFrontendPackageVersion, getVersionInfo } from "./utils/versionUtils.mjs";
|
|
38
|
-
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcAppIndexed, AcAppPageIndexed, AcAppUiIndexed, AcAppsMeta, type AcAuthConfig, AcBackendAppDto, AcBackendAppInput, AcBackendAppUIBaseInput, AcBackendAppUIInput, AcBackendCredentialInput, AcBackendDataFreshness, AcBackendDataSourceInput, AcBackendDataSourceInputCommon, AcBackendDataSourceInputDb, AcBackendDataSourceInputKafka, AcBackendDataVersion, AcBackendDeployableInput, AcBackendDeployment, AcBackendDeploymentInput, AcBackendEnvironmentInput, AcBackendPageInput, type AcBackendProvider, AcBackendTagDescriptionDataIndexed, AcBackendTagFixedTagValue, AcBackendTagsDescriptionDataIndexed, AcBackendUiDefaultsInput, AcBackendVersionsRequestParams, AcBackendVersionsReturn, AcContextIndexed, type AcDatabaseConfig, AcDatabaseManager, AcEnvIndexed, type AcFeatureToggles, type AcLifecycleHooks, type AcLighthouseKeeperConfig, type AcMcpServerConfig, AcMetaDictionary, type AcMiddlewareOptions, type AcMiddlewareResult, AcResourceIndexed, type AcStaticControllerContract, type AcTrpcContext, type AcTrpcContextOptions, AppAccessRequest, AppApprovalMethod, AppCatalogCompanySpecificBackend, AppCatalogData, AppCategory, type AppForCatalog, AppRole, AppTierVariant, AppVersionInfo, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, type AssetRestControllerConfig, type AuthRouter, type BetterAuth,
|
|
38
|
+
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcAppIndexed, AcAppPageIndexed, AcAppUiIndexed, AcAppsMeta, type AcAuthConfig, AcBackendAppDto, AcBackendAppInput, AcBackendAppUIBaseInput, AcBackendAppUIInput, AcBackendCredentialInput, AcBackendDataFreshness, AcBackendDataSourceInput, AcBackendDataSourceInputCommon, AcBackendDataSourceInputDb, AcBackendDataSourceInputKafka, AcBackendDataVersion, AcBackendDeployableInput, AcBackendDeployment, AcBackendDeploymentInput, AcBackendEnvironmentInput, AcBackendPageInput, type AcBackendProvider, AcBackendTagDescriptionDataIndexed, AcBackendTagFixedTagValue, AcBackendTagsDescriptionDataIndexed, AcBackendUiDefaultsInput, AcBackendVersionsRequestParams, AcBackendVersionsReturn, AcContextIndexed, type AcDatabaseConfig, AcDatabaseManager, AcEnvIndexed, type AcFeatureToggles, type AcLifecycleHooks, type AcLighthouseKeeperConfig, type AcMcpServerConfig, AcMetaDictionary, type AcMiddlewareOptions, type AcMiddlewareResult, AcResourceIndexed, type AcStaticControllerContract, type AcTrpcContext, type AcTrpcContextOptions, AppAccessRequest, AppApprovalMethod, AppCatalogCompanySpecificBackend, AppCatalogData, AppCategory, type AppForCatalog, AppRole, AppTierVariant, AppVersionInfo, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, type AssetRestControllerConfig, type AuthRouter, type BetterAuth, CustomConfig, Group, GroupingTagDefinition, GroupingTagValue, type IconRestControllerConfig, type MakeTFromPrismaModel, type MiddlewareContext, type ObjectKeys, Person, PersonTeamConfig, type ScalarFilter, type ScalarKeys, type ScreenshotRestControllerConfig, ServiceConfig, SourceReference, SubResource, type SyncAppCatalogResult, type SyncAssetsConfig, TABLE_SYNC_MAGAZINE, type TRPCRouter, type TableSyncMagazine, type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma, Tag, type UpsertIconInput, VersionInfo, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getVersionInfo, injectCustomScripts, printLinkCheckReport, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, runLighthouseKeeperDemo, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, upsertIcon, upsertIcons };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkLinks.d.mts","names":[],"sources":["../../../src/modules/appCatalog/checkLinks.ts"],"mappings":";UAGU,SAAA;EACR,GAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA;EACA,QAAA;AAAA;AAAA,UAGQ,iBAAA;EACR,aAAA;EACA,OAAA;EACA,UAAA;EACA,UAAA,IAAc,MAAA,EAAQ,SAAA;AAAA;;;;
|
|
1
|
+
{"version":3,"file":"checkLinks.d.mts","names":[],"sources":["../../../src/modules/appCatalog/checkLinks.ts"],"mappings":";UAGU,SAAA;EACR,GAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA;EACA,QAAA;AAAA;AAAA,UAGQ,iBAAA;EACR,aAAA;EACA,OAAA;EACA,UAAA;EACA,UAAA,IAAc,MAAA,EAAQ,SAAA;AAAA;;;;iBAuLF,aAAA,CAAc,OAAA,GAAS,iBAAA,GAAyB,OAAA;EACpE,KAAA;EACA,OAAA;EACA,MAAA;EACA,SAAA;EACA,MAAA,EAAQ,SAAA;AAAA;;AALV;;iBAiHgB,oBAAA,CAAqB,MAAA;EACnC,KAAA;EACA,OAAA;EACA,MAAA;EACA,SAAA;EACA,MAAA,EAAQ,SAAA;AAAA"}
|
|
@@ -4,6 +4,35 @@ import { getDbClient } from "../../db/client.mjs";
|
|
|
4
4
|
async function sleep(ms) {
|
|
5
5
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
6
|
}
|
|
7
|
+
function classifyNetworkError(err) {
|
|
8
|
+
const cause = err instanceof Error && "cause" in err && err.cause instanceof Error ? err.cause : null;
|
|
9
|
+
const hasCode = (obj) => obj != null && typeof obj === "object" && "code" in obj;
|
|
10
|
+
const code = (hasCode(cause) ? cause.code : null) ?? (hasCode(err) ? err.code : null);
|
|
11
|
+
const causeMsg = cause?.message ?? "";
|
|
12
|
+
const topMsg = err instanceof Error ? err.message : String(err);
|
|
13
|
+
if (code) switch (code) {
|
|
14
|
+
case "ENOTFOUND":
|
|
15
|
+
case "EAI_AGAIN": return `DNS resolution failed (${code})`;
|
|
16
|
+
case "ECONNREFUSED": return `Connection refused (${code})`;
|
|
17
|
+
case "ETIMEDOUT":
|
|
18
|
+
case "ECONNRESET": return `Connection timeout (${code})`;
|
|
19
|
+
case "ENETUNREACH":
|
|
20
|
+
case "EHOSTUNREACH": return `Network unreachable (${code})`;
|
|
21
|
+
case "UNABLE_TO_VERIFY_LEAF_SIGNATURE": return `SSL certificate error: unable to verify certificate (${code})`;
|
|
22
|
+
case "CERT_HAS_EXPIRED": return `SSL certificate error: certificate expired (${code})`;
|
|
23
|
+
case "ERR_TLS_CERT_ALTNAME_INVALID": return `SSL certificate error: hostname mismatch (${code})`;
|
|
24
|
+
case "DEPTH_ZERO_SELF_SIGNED_CERT":
|
|
25
|
+
case "SELF_SIGNED_CERT_IN_CHAIN": return `SSL certificate error: self-signed certificate (${code})`;
|
|
26
|
+
default:
|
|
27
|
+
if (code.startsWith("ERR_TLS") || code.startsWith("CERT")) return `SSL/TLS error (${code}): ${causeMsg || topMsg}`;
|
|
28
|
+
return `Network error (${code}): ${causeMsg || topMsg}`;
|
|
29
|
+
}
|
|
30
|
+
const msg = causeMsg || topMsg;
|
|
31
|
+
if (/certificate/i.test(msg) || /SSL|TLS/i.test(msg)) return `SSL/TLS error: ${msg}`;
|
|
32
|
+
if (/abort|timeout/i.test(msg)) return `Request timeout: ${msg}`;
|
|
33
|
+
if (/dns|resolve/i.test(msg)) return `DNS error: ${msg}`;
|
|
34
|
+
return causeMsg || topMsg;
|
|
35
|
+
}
|
|
7
36
|
async function checkUrlWithRetry(url, options) {
|
|
8
37
|
let lastError;
|
|
9
38
|
for (let attempt = 0; attempt <= options.maxRetries; attempt++) try {
|
|
@@ -13,9 +42,9 @@ async function checkUrlWithRetry(url, options) {
|
|
|
13
42
|
signal: AbortSignal.timeout(options.timeout)
|
|
14
43
|
})).status };
|
|
15
44
|
} catch (error) {
|
|
16
|
-
const errorMessage =
|
|
45
|
+
const errorMessage = classifyNetworkError(error);
|
|
17
46
|
lastError = errorMessage;
|
|
18
|
-
if ((errorMessage.includes("
|
|
47
|
+
if ((errorMessage.includes("timeout") || errorMessage.includes("ECONNRESET") || errorMessage.includes("ETIMEDOUT") || errorMessage.includes("EAI_AGAIN")) && attempt < options.maxRetries) {
|
|
19
48
|
await sleep(1e3 * 2 ** attempt);
|
|
20
49
|
continue;
|
|
21
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkLinks.mjs","names":[],"sources":["../../../src/modules/appCatalog/checkLinks.ts"],"sourcesContent":["import type { AppForCatalog } from '../../types/common/appCatalogTypes'\nimport { getDbClient } from '../../db/client'\n\ninterface LinkCheck {\n url: string\n status: number | null\n error?: string\n appSlug: string\n linkType: 'appUrl' | 'sources' | 'accessRequest.urls'\n}\n\ninterface CheckLinksOptions {\n maxConcurrent?: number\n timeout?: number\n maxRetries?: number\n onProgress?: (result: LinkCheck) => void\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nasync function checkUrlWithRetry(\n url: string,\n options: {\n timeout: number\n maxRetries: number\n },\n): Promise<{ status: number | null; error?: string }> {\n let lastError: string | undefined\n\n for (let attempt = 0; attempt <= options.maxRetries; attempt++) {\n try {\n const response = await fetch(url, {\n method: 'HEAD',\n redirect: 'manual', // Don't follow redirects\n signal: AbortSignal.timeout(options.timeout),\n })\n return { status: response.status }\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : String(error)\n lastError = errorMessage\n\n // Check if it's a retryable error (timeout or fetch failed)\n const isRetryable =\n errorMessage.includes('aborted') ||\n errorMessage.includes('timeout') ||\n errorMessage.includes('fetch failed')\n\n if (isRetryable && attempt < options.maxRetries) {\n // Exponential backoff: 1s, 2s, 4s\n const backoffMs = 1000 * 2 ** attempt\n await sleep(backoffMs)\n continue\n }\n\n break\n }\n }\n\n return { status: null, error: lastError }\n}\n\nfunction getStatusEmoji(status: number | null): string {\n if (status === null) return '❌'\n if (status >= 200 && status < 300) return '✅'\n if (status >= 300 && status < 400) return '🔀'\n if (status === 403) return '🔒' // 403 is not an error, just forbidden\n if (status && status >= 400 && status < 500) return '⚠️'\n return '❌'\n}\n\nfunction isWorkingLink(status: number | null): boolean {\n if (status === null) return false\n // 2xx, 3xx, and 403 are considered working\n if (status >= 200 && status < 400) return true\n if (status === 403) return true\n return false\n}\n\nfunction isBrokenLink(status: number | null): boolean {\n if (status === null) return true\n // 403 is not broken\n if (status === 403) return false\n // 4xx (except 403) and 5xx are broken\n return status >= 400\n}\n\nfunction formatStatus(status: number | null, error?: string): string {\n if (status === null) {\n return `ERROR: ${error || 'Unknown error'}`\n }\n return `${status}`\n}\n\nasync function getAppsFromDb(): Promise<AppForCatalog[]> {\n const prisma = getDbClient()\n const rows = await prisma.dbAppForCatalog.findMany({\n include: {\n sourceRefs: true,\n },\n })\n\n return rows.map((row) => {\n const accessRequest =\n row.accessRequest as unknown as AppForCatalog['accessRequest']\n const teams = (row.teams as unknown as string[] | null) ?? []\n const tags = (row.tags as unknown as AppForCatalog['tags']) ?? []\n const screenshotIds =\n (row.screenshotIds as unknown as AppForCatalog['screenshotIds']) ?? []\n const sources = row.sourceRefs.map((ref) => ({\n sourceSlug: ref.sourceSlug,\n url: ref.url,\n parseDate: ref.parseDate ? ref.parseDate.toISOString() : null,\n }))\n const notes = row.notes == null ? undefined : row.notes\n const appUrl = row.appUrl == null ? undefined : row.appUrl\n const iconName = row.iconName == null ? undefined : row.iconName\n const deprecated =\n row.deprecated == null\n ? undefined\n : (row.deprecated as unknown as AppForCatalog['deprecated'])\n\n return {\n id: row.id,\n slug: row.slug,\n displayName: row.displayName,\n description: row.description,\n accessRequest,\n teams,\n notes,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n sources,\n deprecated,\n }\n })\n}\n\n/**\n * Check all links in the app catalog and return a report\n */\nexport async function checkAllLinks(options: CheckLinksOptions = {}): Promise<{\n total: number\n working: number\n broken: number\n redirects: number\n checks: LinkCheck[]\n}> {\n const {\n maxConcurrent = 10,\n timeout = 60000,\n maxRetries = 3,\n onProgress,\n } = options\n\n const apps = await getAppsFromDb()\n const checks: Omit<LinkCheck, 'status' | 'error'>[] = []\n\n // Collect all links\n for (const app of apps) {\n // Check appUrl\n if (app.appUrl) {\n checks.push({\n url: app.appUrl,\n appSlug: app.slug,\n linkType: 'appUrl',\n })\n }\n\n // Check sources\n if (app.sources && app.sources.length > 0) {\n for (const source of app.sources) {\n const url = typeof source === 'string' ? source : source.url\n checks.push({\n url,\n appSlug: app.slug,\n linkType: 'sources',\n })\n }\n }\n\n // Check accessRequest.urls\n if (app.accessRequest?.urls && app.accessRequest.urls.length > 0) {\n for (const link of app.accessRequest.urls) {\n checks.push({\n url: link.url,\n appSlug: app.slug,\n linkType: 'accessRequest.urls',\n })\n }\n }\n }\n\n // Check all links with concurrency control\n const results: LinkCheck[] = []\n const queue = [...checks]\n const inProgress = new Set<Promise<void>>()\n\n while (queue.length > 0 || inProgress.size > 0) {\n // Start new requests up to maxConcurrent\n while (queue.length > 0 && inProgress.size < maxConcurrent) {\n const check = queue.shift()!\n\n const promise = (async () => {\n const result = await checkUrlWithRetry(check.url, {\n timeout,\n maxRetries,\n })\n\n const linkCheck: LinkCheck = {\n ...check,\n status: result.status,\n error: result.error,\n }\n results.push(linkCheck)\n\n // Stream result if callback provided\n if (onProgress) {\n onProgress(linkCheck)\n }\n })()\n\n inProgress.add(promise)\n\n // Clean up when promise completes\n void promise.finally(() => {\n inProgress.delete(promise)\n })\n }\n\n // Wait for at least one to complete before continuing\n if (inProgress.size > 0) {\n await Promise.race(inProgress)\n }\n }\n\n const working = results.filter((r) => isWorkingLink(r.status))\n const broken = results.filter((r) => isBrokenLink(r.status))\n const redirects = results.filter(\n (r) => r.status && r.status >= 300 && r.status < 400,\n )\n\n return {\n total: results.length,\n working: working.length,\n broken: broken.length,\n redirects: redirects.length,\n checks: results,\n }\n}\n\n/**\n * Print a formatted report of link check results\n */\nexport function printLinkCheckReport(report: {\n total: number\n working: number\n broken: number\n redirects: number\n checks: LinkCheck[]\n}): void {\n console.log('📊 Results:\\n')\n console.log(`✅ Working links: ${report.working}`)\n console.log(`❌ Broken links: ${report.broken}`)\n console.log(`🔀 Redirects: ${report.redirects}`)\n console.log()\n\n const broken = report.checks.filter((r) => isBrokenLink(r.status))\n const redirects = report.checks.filter(\n (r) => r.status && r.status >= 300 && r.status < 400,\n )\n\n // Show broken links\n if (broken.length > 0) {\n console.log('❌ Broken Links:\\n')\n for (const link of broken) {\n console.log(\n ` ${getStatusEmoji(link.status)} [${link.appSlug}] ${link.linkType}`,\n )\n console.log(` ${link.url}`)\n console.log(` Status: ${formatStatus(link.status, link.error)}`)\n console.log()\n }\n }\n\n // Show redirects\n if (redirects.length > 0) {\n console.log('🔀 Redirects:\\n')\n for (const link of redirects) {\n console.log(\n ` ${getStatusEmoji(link.status)} [${link.appSlug}] ${link.linkType}`,\n )\n console.log(` ${link.url}`)\n console.log(` Status: ${formatStatus(link.status, link.error)}`)\n console.log()\n }\n }\n\n // Summary by app\n const appStats = new Map<\n string,\n { working: number; broken: number; total: number }\n >()\n for (const result of report.checks) {\n const stats = appStats.get(result.appSlug) || {\n working: 0,\n broken: 0,\n total: 0,\n }\n stats.total++\n if (isWorkingLink(result.status)) {\n stats.working++\n } else {\n stats.broken++\n }\n appStats.set(result.appSlug, stats)\n }\n\n const appsWithBrokenLinks = Array.from(appStats.entries())\n .filter(([_, stats]) => stats.broken > 0)\n .sort((a, b) => b[1].broken - a[1].broken)\n\n if (appsWithBrokenLinks.length > 0) {\n console.log('📱 Apps with broken links:\\n')\n for (const [appSlug, stats] of appsWithBrokenLinks) {\n console.log(` ${appSlug}: ${stats.broken}/${stats.total} broken`)\n }\n console.log()\n }\n\n if (broken.length > 0) {\n console.log(`\\n❌ Found ${broken.length} broken link(s)`)\n } else {\n console.log('\\n✅ All links are working!')\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"checkLinks.mjs","names":[],"sources":["../../../src/modules/appCatalog/checkLinks.ts"],"sourcesContent":["import type { AppForCatalog } from '../../types/common/appCatalogTypes'\nimport { getDbClient } from '../../db/client'\n\ninterface LinkCheck {\n url: string\n status: number | null\n error?: string\n appSlug: string\n linkType: 'appUrl' | 'sources' | 'accessRequest.urls'\n}\n\ninterface CheckLinksOptions {\n maxConcurrent?: number\n timeout?: number\n maxRetries?: number\n onProgress?: (result: LinkCheck) => void\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction classifyNetworkError(err: unknown): string {\n // Node's fetch wraps the real error in error.cause\n const cause =\n err instanceof Error && 'cause' in err && err.cause instanceof Error\n ? err.cause\n : null\n\n const hasCode = (obj: unknown): obj is { code: string } =>\n obj != null && typeof obj === 'object' && 'code' in obj\n\n const code =\n (hasCode(cause) ? cause.code : null) ?? (hasCode(err) ? err.code : null)\n const causeMsg = cause?.message ?? ''\n const topMsg = err instanceof Error ? err.message : String(err)\n\n if (code) {\n switch (code) {\n case 'ENOTFOUND':\n case 'EAI_AGAIN':\n return `DNS resolution failed (${code})`\n case 'ECONNREFUSED':\n return `Connection refused (${code})`\n case 'ETIMEDOUT':\n case 'ECONNRESET':\n return `Connection timeout (${code})`\n case 'ENETUNREACH':\n case 'EHOSTUNREACH':\n return `Network unreachable (${code})`\n case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':\n return `SSL certificate error: unable to verify certificate (${code})`\n case 'CERT_HAS_EXPIRED':\n return `SSL certificate error: certificate expired (${code})`\n case 'ERR_TLS_CERT_ALTNAME_INVALID':\n return `SSL certificate error: hostname mismatch (${code})`\n case 'DEPTH_ZERO_SELF_SIGNED_CERT':\n case 'SELF_SIGNED_CERT_IN_CHAIN':\n return `SSL certificate error: self-signed certificate (${code})`\n default:\n if (code.startsWith('ERR_TLS') || code.startsWith('CERT'))\n return `SSL/TLS error (${code}): ${causeMsg || topMsg}`\n return `Network error (${code}): ${causeMsg || topMsg}`\n }\n }\n\n const msg = causeMsg || topMsg\n if (/certificate/i.test(msg) || /SSL|TLS/i.test(msg))\n return `SSL/TLS error: ${msg}`\n if (/abort|timeout/i.test(msg)) return `Request timeout: ${msg}`\n if (/dns|resolve/i.test(msg)) return `DNS error: ${msg}`\n\n return causeMsg || topMsg\n}\n\nasync function checkUrlWithRetry(\n url: string,\n options: {\n timeout: number\n maxRetries: number\n },\n): Promise<{ status: number | null; error?: string }> {\n let lastError: string | undefined\n\n for (let attempt = 0; attempt <= options.maxRetries; attempt++) {\n try {\n const response = await fetch(url, {\n method: 'HEAD',\n redirect: 'manual', // Don't follow redirects\n signal: AbortSignal.timeout(options.timeout),\n })\n return { status: response.status }\n } catch (error: unknown) {\n const errorMessage = classifyNetworkError(error)\n lastError = errorMessage\n\n // Check if it's a retryable error (timeout or transient network issues)\n const isRetryable =\n errorMessage.includes('timeout') ||\n errorMessage.includes('ECONNRESET') ||\n errorMessage.includes('ETIMEDOUT') ||\n errorMessage.includes('EAI_AGAIN')\n\n if (isRetryable && attempt < options.maxRetries) {\n // Exponential backoff: 1s, 2s, 4s\n const backoffMs = 1000 * 2 ** attempt\n await sleep(backoffMs)\n continue\n }\n\n break\n }\n }\n\n return { status: null, error: lastError }\n}\n\nfunction getStatusEmoji(status: number | null): string {\n if (status === null) return '❌'\n if (status >= 200 && status < 300) return '✅'\n if (status >= 300 && status < 400) return '🔀'\n if (status === 403) return '🔒' // 403 is not an error, just forbidden\n if (status && status >= 400 && status < 500) return '⚠️'\n return '❌'\n}\n\nfunction isWorkingLink(status: number | null): boolean {\n if (status === null) return false\n // 2xx, 3xx, and 403 are considered working\n if (status >= 200 && status < 400) return true\n if (status === 403) return true\n return false\n}\n\nfunction isBrokenLink(status: number | null): boolean {\n if (status === null) return true\n // 403 is not broken\n if (status === 403) return false\n // 4xx (except 403) and 5xx are broken\n return status >= 400\n}\n\nfunction formatStatus(status: number | null, error?: string): string {\n if (status === null) {\n return `ERROR: ${error || 'Unknown error'}`\n }\n return `${status}`\n}\n\nasync function getAppsFromDb(): Promise<AppForCatalog[]> {\n const prisma = getDbClient()\n const rows = await prisma.dbAppForCatalog.findMany({\n include: {\n sourceRefs: true,\n },\n })\n\n return rows.map((row) => {\n const accessRequest =\n row.accessRequest as unknown as AppForCatalog['accessRequest']\n const teams = (row.teams as unknown as string[] | null) ?? []\n const tags = (row.tags as unknown as AppForCatalog['tags']) ?? []\n const screenshotIds =\n (row.screenshotIds as unknown as AppForCatalog['screenshotIds']) ?? []\n const sources = row.sourceRefs.map((ref) => ({\n sourceSlug: ref.sourceSlug,\n url: ref.url,\n parseDate: ref.parseDate ? ref.parseDate.toISOString() : null,\n }))\n const notes = row.notes == null ? undefined : row.notes\n const appUrl = row.appUrl == null ? undefined : row.appUrl\n const iconName = row.iconName == null ? undefined : row.iconName\n const deprecated =\n row.deprecated == null\n ? undefined\n : (row.deprecated as unknown as AppForCatalog['deprecated'])\n\n return {\n id: row.id,\n slug: row.slug,\n displayName: row.displayName,\n description: row.description,\n accessRequest,\n teams,\n notes,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n sources,\n deprecated,\n }\n })\n}\n\n/**\n * Check all links in the app catalog and return a report\n */\nexport async function checkAllLinks(options: CheckLinksOptions = {}): Promise<{\n total: number\n working: number\n broken: number\n redirects: number\n checks: LinkCheck[]\n}> {\n const {\n maxConcurrent = 10,\n timeout = 60000,\n maxRetries = 3,\n onProgress,\n } = options\n\n const apps = await getAppsFromDb()\n const checks: Omit<LinkCheck, 'status' | 'error'>[] = []\n\n // Collect all links\n for (const app of apps) {\n // Check appUrl\n if (app.appUrl) {\n checks.push({\n url: app.appUrl,\n appSlug: app.slug,\n linkType: 'appUrl',\n })\n }\n\n // Check sources\n if (app.sources && app.sources.length > 0) {\n for (const source of app.sources) {\n const url = typeof source === 'string' ? source : source.url\n checks.push({\n url,\n appSlug: app.slug,\n linkType: 'sources',\n })\n }\n }\n\n // Check accessRequest.urls\n if (app.accessRequest?.urls && app.accessRequest.urls.length > 0) {\n for (const link of app.accessRequest.urls) {\n checks.push({\n url: link.url,\n appSlug: app.slug,\n linkType: 'accessRequest.urls',\n })\n }\n }\n }\n\n // Check all links with concurrency control\n const results: LinkCheck[] = []\n const queue = [...checks]\n const inProgress = new Set<Promise<void>>()\n\n while (queue.length > 0 || inProgress.size > 0) {\n // Start new requests up to maxConcurrent\n while (queue.length > 0 && inProgress.size < maxConcurrent) {\n const check = queue.shift()!\n\n const promise = (async () => {\n const result = await checkUrlWithRetry(check.url, {\n timeout,\n maxRetries,\n })\n\n const linkCheck: LinkCheck = {\n ...check,\n status: result.status,\n error: result.error,\n }\n results.push(linkCheck)\n\n // Stream result if callback provided\n if (onProgress) {\n onProgress(linkCheck)\n }\n })()\n\n inProgress.add(promise)\n\n // Clean up when promise completes\n void promise.finally(() => {\n inProgress.delete(promise)\n })\n }\n\n // Wait for at least one to complete before continuing\n if (inProgress.size > 0) {\n await Promise.race(inProgress)\n }\n }\n\n const working = results.filter((r) => isWorkingLink(r.status))\n const broken = results.filter((r) => isBrokenLink(r.status))\n const redirects = results.filter(\n (r) => r.status && r.status >= 300 && r.status < 400,\n )\n\n return {\n total: results.length,\n working: working.length,\n broken: broken.length,\n redirects: redirects.length,\n checks: results,\n }\n}\n\n/**\n * Print a formatted report of link check results\n */\nexport function printLinkCheckReport(report: {\n total: number\n working: number\n broken: number\n redirects: number\n checks: LinkCheck[]\n}): void {\n console.log('📊 Results:\\n')\n console.log(`✅ Working links: ${report.working}`)\n console.log(`❌ Broken links: ${report.broken}`)\n console.log(`🔀 Redirects: ${report.redirects}`)\n console.log()\n\n const broken = report.checks.filter((r) => isBrokenLink(r.status))\n const redirects = report.checks.filter(\n (r) => r.status && r.status >= 300 && r.status < 400,\n )\n\n // Show broken links\n if (broken.length > 0) {\n console.log('❌ Broken Links:\\n')\n for (const link of broken) {\n console.log(\n ` ${getStatusEmoji(link.status)} [${link.appSlug}] ${link.linkType}`,\n )\n console.log(` ${link.url}`)\n console.log(` Status: ${formatStatus(link.status, link.error)}`)\n console.log()\n }\n }\n\n // Show redirects\n if (redirects.length > 0) {\n console.log('🔀 Redirects:\\n')\n for (const link of redirects) {\n console.log(\n ` ${getStatusEmoji(link.status)} [${link.appSlug}] ${link.linkType}`,\n )\n console.log(` ${link.url}`)\n console.log(` Status: ${formatStatus(link.status, link.error)}`)\n console.log()\n }\n }\n\n // Summary by app\n const appStats = new Map<\n string,\n { working: number; broken: number; total: number }\n >()\n for (const result of report.checks) {\n const stats = appStats.get(result.appSlug) || {\n working: 0,\n broken: 0,\n total: 0,\n }\n stats.total++\n if (isWorkingLink(result.status)) {\n stats.working++\n } else {\n stats.broken++\n }\n appStats.set(result.appSlug, stats)\n }\n\n const appsWithBrokenLinks = Array.from(appStats.entries())\n .filter(([_, stats]) => stats.broken > 0)\n .sort((a, b) => b[1].broken - a[1].broken)\n\n if (appsWithBrokenLinks.length > 0) {\n console.log('📱 Apps with broken links:\\n')\n for (const [appSlug, stats] of appsWithBrokenLinks) {\n console.log(` ${appSlug}: ${stats.broken}/${stats.total} broken`)\n }\n console.log()\n }\n\n if (broken.length > 0) {\n console.log(`\\n❌ Found ${broken.length} broken link(s)`)\n } else {\n console.log('\\n✅ All links are working!')\n }\n}\n"],"mappings":"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"}
|
|
@@ -33,7 +33,13 @@ type ApprovalMethodConfig = ServiceConfig | PersonTeamConfig | CustomConfig;
|
|
|
33
33
|
*/
|
|
34
34
|
type ApprovalMethod = {
|
|
35
35
|
slug: string;
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Displayed approval method
|
|
38
|
+
*/
|
|
39
|
+
displayName: string;
|
|
40
|
+
/**
|
|
41
|
+
* Optionally - older name of approval method if there were migration in organization.
|
|
42
|
+
*/
|
|
37
43
|
deprecatedAliases?: string[];
|
|
38
44
|
createdAt?: Date;
|
|
39
45
|
updatedAt?: Date;
|
|
@@ -57,8 +63,17 @@ type ApprovalMethod = {
|
|
|
57
63
|
* Role that can be requested for an app
|
|
58
64
|
*/
|
|
59
65
|
interface AppRole {
|
|
66
|
+
/**
|
|
67
|
+
* User-friendly role name.
|
|
68
|
+
*/
|
|
60
69
|
displayName: string;
|
|
70
|
+
/**
|
|
71
|
+
* Description of role.
|
|
72
|
+
*/
|
|
61
73
|
description?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Notes for admins/approvers (Not for requestores)
|
|
76
|
+
*/
|
|
62
77
|
adminNotes?: string;
|
|
63
78
|
}
|
|
64
79
|
/**
|
|
@@ -69,29 +84,38 @@ interface ApprovalUrl {
|
|
|
69
84
|
url: string;
|
|
70
85
|
}
|
|
71
86
|
/**
|
|
72
|
-
*
|
|
73
|
-
* All comment/text-like strings are markdown
|
|
87
|
+
* Used to store ONLY instructions related to access request
|
|
74
88
|
*/
|
|
75
89
|
interface AppAccessRequest {
|
|
90
|
+
/**
|
|
91
|
+
* Method of asking for access.
|
|
92
|
+
*/
|
|
76
93
|
approvalMethodSlug: string;
|
|
94
|
+
/**
|
|
95
|
+
* Additional comments, if no other fields are fit.
|
|
96
|
+
*/
|
|
77
97
|
comments?: string;
|
|
98
|
+
/**
|
|
99
|
+
* A template to put into a request ask.
|
|
100
|
+
*/
|
|
78
101
|
requestPrompt?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Recommended steps post approvel to get access to specific resources.
|
|
104
|
+
*/
|
|
79
105
|
postApprovalInstructions?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Available roles for given resource.
|
|
108
|
+
*/
|
|
80
109
|
roles?: AppRole[];
|
|
110
|
+
/**
|
|
111
|
+
* Individuals that will approve request within given approval method. No need to reach them directly unless specified.
|
|
112
|
+
*/
|
|
81
113
|
approverPersonSlugs?: string[];
|
|
114
|
+
/**
|
|
115
|
+
* Additional instructions to get approvals
|
|
116
|
+
*/
|
|
82
117
|
urls?: ApprovalUrl[];
|
|
83
118
|
}
|
|
84
|
-
interface CreateApprovalMethodInput {
|
|
85
|
-
type: ApprovalMethodType;
|
|
86
|
-
displayName: string;
|
|
87
|
-
config?: ApprovalMethodConfig;
|
|
88
|
-
}
|
|
89
|
-
interface UpdateApprovalMethodInput {
|
|
90
|
-
id: string;
|
|
91
|
-
type?: ApprovalMethodType;
|
|
92
|
-
displayName?: string;
|
|
93
|
-
config?: ApprovalMethodConfig;
|
|
94
|
-
}
|
|
95
119
|
//#endregion
|
|
96
|
-
export { AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl,
|
|
120
|
+
export { AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, ServiceConfig };
|
|
97
121
|
//# sourceMappingURL=approvalMethodTypes.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;AAAA;;;AAcF;UARiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;AAaF;;;AAAA,UAPiB,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;
|
|
1
|
+
{"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;AAAA;;;AAcF;UARiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;AAaF;;;AAAA,UAPiB,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;EADwB;;;EAKxB,WAAA;EAUY;;;EANZ,iBAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,aAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;;UAWG,OAAA;EAZX;;;EAgBJ,WAAA;EAfwB;AAW1B;;EAQE,WAAA;EARsB;;;EAYtB,UAAA;AAAA;;AAMF;;UAAiB,WAAA;EACf,KAAA;EACA,GAAA;AAAA;;;;UAMe,gBAAA;EASf;;;EALA,kBAAA;EAkBQ;;;EAbR,QAAA;EAsBkB;;;EAlBlB,aAAA;;;;EAIA,wBAAA;;;;EAKA,KAAA,GAAQ,OAAA;;;;EAKR,mBAAA;;;;EAIA,IAAA,GAAO,WAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-backend-core",
|
|
3
|
-
"version": "0.3.1-alpha-
|
|
3
|
+
"version": "0.3.1-alpha-20260405015231",
|
|
4
4
|
"description": "Backend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"tsyringe": "^4.10.0",
|
|
46
46
|
"yaml": "^2.8.0",
|
|
47
47
|
"zod": "^4.3.5",
|
|
48
|
-
"@igstack/app-catalog-shared-core": "0.3.1-alpha-
|
|
49
|
-
"@igstack/app-catalog-table-sync": "0.3.1-alpha-
|
|
48
|
+
"@igstack/app-catalog-shared-core": "0.3.1-alpha-20260405015231",
|
|
49
|
+
"@igstack/app-catalog-table-sync": "0.3.1-alpha-20260405015231"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tanstack/vite-config": "^0.4.3",
|
|
@@ -20,6 +20,59 @@ async function sleep(ms: number): Promise<void> {
|
|
|
20
20
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function classifyNetworkError(err: unknown): string {
|
|
24
|
+
// Node's fetch wraps the real error in error.cause
|
|
25
|
+
const cause =
|
|
26
|
+
err instanceof Error && 'cause' in err && err.cause instanceof Error
|
|
27
|
+
? err.cause
|
|
28
|
+
: null
|
|
29
|
+
|
|
30
|
+
const hasCode = (obj: unknown): obj is { code: string } =>
|
|
31
|
+
obj != null && typeof obj === 'object' && 'code' in obj
|
|
32
|
+
|
|
33
|
+
const code =
|
|
34
|
+
(hasCode(cause) ? cause.code : null) ?? (hasCode(err) ? err.code : null)
|
|
35
|
+
const causeMsg = cause?.message ?? ''
|
|
36
|
+
const topMsg = err instanceof Error ? err.message : String(err)
|
|
37
|
+
|
|
38
|
+
if (code) {
|
|
39
|
+
switch (code) {
|
|
40
|
+
case 'ENOTFOUND':
|
|
41
|
+
case 'EAI_AGAIN':
|
|
42
|
+
return `DNS resolution failed (${code})`
|
|
43
|
+
case 'ECONNREFUSED':
|
|
44
|
+
return `Connection refused (${code})`
|
|
45
|
+
case 'ETIMEDOUT':
|
|
46
|
+
case 'ECONNRESET':
|
|
47
|
+
return `Connection timeout (${code})`
|
|
48
|
+
case 'ENETUNREACH':
|
|
49
|
+
case 'EHOSTUNREACH':
|
|
50
|
+
return `Network unreachable (${code})`
|
|
51
|
+
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
|
|
52
|
+
return `SSL certificate error: unable to verify certificate (${code})`
|
|
53
|
+
case 'CERT_HAS_EXPIRED':
|
|
54
|
+
return `SSL certificate error: certificate expired (${code})`
|
|
55
|
+
case 'ERR_TLS_CERT_ALTNAME_INVALID':
|
|
56
|
+
return `SSL certificate error: hostname mismatch (${code})`
|
|
57
|
+
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
|
58
|
+
case 'SELF_SIGNED_CERT_IN_CHAIN':
|
|
59
|
+
return `SSL certificate error: self-signed certificate (${code})`
|
|
60
|
+
default:
|
|
61
|
+
if (code.startsWith('ERR_TLS') || code.startsWith('CERT'))
|
|
62
|
+
return `SSL/TLS error (${code}): ${causeMsg || topMsg}`
|
|
63
|
+
return `Network error (${code}): ${causeMsg || topMsg}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const msg = causeMsg || topMsg
|
|
68
|
+
if (/certificate/i.test(msg) || /SSL|TLS/i.test(msg))
|
|
69
|
+
return `SSL/TLS error: ${msg}`
|
|
70
|
+
if (/abort|timeout/i.test(msg)) return `Request timeout: ${msg}`
|
|
71
|
+
if (/dns|resolve/i.test(msg)) return `DNS error: ${msg}`
|
|
72
|
+
|
|
73
|
+
return causeMsg || topMsg
|
|
74
|
+
}
|
|
75
|
+
|
|
23
76
|
async function checkUrlWithRetry(
|
|
24
77
|
url: string,
|
|
25
78
|
options: {
|
|
@@ -38,15 +91,15 @@ async function checkUrlWithRetry(
|
|
|
38
91
|
})
|
|
39
92
|
return { status: response.status }
|
|
40
93
|
} catch (error: unknown) {
|
|
41
|
-
const errorMessage =
|
|
42
|
-
error instanceof Error ? error.message : String(error)
|
|
94
|
+
const errorMessage = classifyNetworkError(error)
|
|
43
95
|
lastError = errorMessage
|
|
44
96
|
|
|
45
|
-
// Check if it's a retryable error (timeout or
|
|
97
|
+
// Check if it's a retryable error (timeout or transient network issues)
|
|
46
98
|
const isRetryable =
|
|
47
|
-
errorMessage.includes('aborted') ||
|
|
48
99
|
errorMessage.includes('timeout') ||
|
|
49
|
-
errorMessage.includes('
|
|
100
|
+
errorMessage.includes('ECONNRESET') ||
|
|
101
|
+
errorMessage.includes('ETIMEDOUT') ||
|
|
102
|
+
errorMessage.includes('EAI_AGAIN')
|
|
50
103
|
|
|
51
104
|
if (isRetryable && attempt < options.maxRetries) {
|
|
52
105
|
// Exponential backoff: 1s, 2s, 4s
|
|
@@ -52,8 +52,13 @@ export type ApprovalMethodConfig =
|
|
|
52
52
|
*/
|
|
53
53
|
export type ApprovalMethod = {
|
|
54
54
|
slug: string
|
|
55
|
+
/**
|
|
56
|
+
* Displayed approval method
|
|
57
|
+
*/
|
|
55
58
|
displayName: string
|
|
56
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Optionally - older name of approval method if there were migration in organization.
|
|
61
|
+
*/
|
|
57
62
|
deprecatedAliases?: string[]
|
|
58
63
|
createdAt?: Date
|
|
59
64
|
updatedAt?: Date
|
|
@@ -88,8 +93,17 @@ export type ApprovalMethod = {
|
|
|
88
93
|
* Role that can be requested for an app
|
|
89
94
|
*/
|
|
90
95
|
export interface AppRole {
|
|
96
|
+
/**
|
|
97
|
+
* User-friendly role name.
|
|
98
|
+
*/
|
|
91
99
|
displayName: string
|
|
100
|
+
/**
|
|
101
|
+
* Description of role.
|
|
102
|
+
*/
|
|
92
103
|
description?: string
|
|
104
|
+
/**
|
|
105
|
+
* Notes for admins/approvers (Not for requestores)
|
|
106
|
+
*/
|
|
93
107
|
adminNotes?: string
|
|
94
108
|
}
|
|
95
109
|
|
|
@@ -102,36 +116,38 @@ export interface ApprovalUrl {
|
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/**
|
|
105
|
-
*
|
|
106
|
-
* All comment/text-like strings are markdown
|
|
119
|
+
* Used to store ONLY instructions related to access request
|
|
107
120
|
*/
|
|
108
121
|
export interface AppAccessRequest {
|
|
122
|
+
/**
|
|
123
|
+
* Method of asking for access.
|
|
124
|
+
*/
|
|
109
125
|
approvalMethodSlug: string // FK to DbApprovalMethod
|
|
110
126
|
|
|
111
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Additional comments, if no other fields are fit.
|
|
129
|
+
*/
|
|
112
130
|
comments?: string
|
|
131
|
+
/**
|
|
132
|
+
* A template to put into a request ask.
|
|
133
|
+
*/
|
|
113
134
|
requestPrompt?: string
|
|
135
|
+
/**
|
|
136
|
+
* Recommended steps post approvel to get access to specific resources.
|
|
137
|
+
*/
|
|
114
138
|
postApprovalInstructions?: string
|
|
115
139
|
|
|
116
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Available roles for given resource.
|
|
142
|
+
*/
|
|
117
143
|
roles?: AppRole[]
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Individuals that will approve request within given approval method. No need to reach them directly unless specified.
|
|
147
|
+
*/
|
|
118
148
|
approverPersonSlugs?: string[] // slugs referencing Person entities
|
|
149
|
+
/**
|
|
150
|
+
* Additional instructions to get approvals
|
|
151
|
+
*/
|
|
119
152
|
urls?: ApprovalUrl[]
|
|
120
153
|
}
|
|
121
|
-
|
|
122
|
-
// ============================================================================
|
|
123
|
-
// INPUT TYPES FOR API
|
|
124
|
-
// ============================================================================
|
|
125
|
-
|
|
126
|
-
export interface CreateApprovalMethodInput {
|
|
127
|
-
type: ApprovalMethodType
|
|
128
|
-
displayName: string
|
|
129
|
-
config?: ApprovalMethodConfig
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface UpdateApprovalMethodInput {
|
|
133
|
-
id: string
|
|
134
|
-
type?: ApprovalMethodType
|
|
135
|
-
displayName?: string
|
|
136
|
-
config?: ApprovalMethodConfig
|
|
137
|
-
}
|