auramaxx 1.0.0-alpha.4
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/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Engine — Public API
|
|
3
|
+
* ============================
|
|
4
|
+
* Re-exports the main engine functions for use by routes and server startup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
startEngine,
|
|
9
|
+
stopEngine,
|
|
10
|
+
enableStrategy,
|
|
11
|
+
disableStrategy,
|
|
12
|
+
reloadStrategies,
|
|
13
|
+
getStrategies,
|
|
14
|
+
getRuntime,
|
|
15
|
+
resolveApproval,
|
|
16
|
+
getPendingApprovals,
|
|
17
|
+
isEngineStarted,
|
|
18
|
+
emitStrategyEvent,
|
|
19
|
+
runExternalTickCycle,
|
|
20
|
+
reconcileWorkspaceStrategies,
|
|
21
|
+
persistEngineStateSnapshot,
|
|
22
|
+
enqueueAppMessage,
|
|
23
|
+
waitForQueuedAppMessage,
|
|
24
|
+
processPendingAppMessages,
|
|
25
|
+
STRATEGY_ENABLED_STORAGE_KEY,
|
|
26
|
+
} from './engine';
|
|
27
|
+
export { getState, updateState, getConfigOverrides, setConfigOverrides } from './state';
|
|
28
|
+
export type { StrategyManifest, StrategyRuntime, StrategyStatus, TickTier, Intent, Action, ActionOutcome, HookResult } from './types';
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
|
+
import { parse as parseYaml } from 'yaml';
|
|
7
|
+
|
|
8
|
+
import { validateExternalUrl } from '../network';
|
|
9
|
+
import { orderSources, validateManifest } from './loader';
|
|
10
|
+
import type { StrategyManifest, SourceDef } from './types';
|
|
11
|
+
|
|
12
|
+
type StrategySourceType = 'git' | 'tarball' | 'zip' | 'local' | 'inline';
|
|
13
|
+
|
|
14
|
+
interface ParsedSource {
|
|
15
|
+
type: 'git' | 'tarball' | 'zip' | 'local';
|
|
16
|
+
url: string;
|
|
17
|
+
subdir: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StrategyInstallProvenance {
|
|
21
|
+
sourceType: StrategySourceType;
|
|
22
|
+
sourceUrl: string;
|
|
23
|
+
ref: string | null;
|
|
24
|
+
subdir: string | null;
|
|
25
|
+
hash: string;
|
|
26
|
+
signaturePresent: boolean;
|
|
27
|
+
installedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PreparedThirdPartyStrategy {
|
|
31
|
+
manifest: StrategyManifest;
|
|
32
|
+
provenance: StrategyInstallProvenance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseSource(source: string): ParsedSource {
|
|
36
|
+
let subdir: string | null = null;
|
|
37
|
+
const hashIdx = source.indexOf('#path=');
|
|
38
|
+
let cleanSource = source;
|
|
39
|
+
if (hashIdx !== -1) {
|
|
40
|
+
subdir = source.slice(hashIdx + 6);
|
|
41
|
+
cleanSource = source.slice(0, hashIdx);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (cleanSource.startsWith('.') || cleanSource.startsWith('/')) {
|
|
45
|
+
return { type: 'local', url: cleanSource, subdir };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (cleanSource.endsWith('.tar.gz') || cleanSource.endsWith('.tgz')) {
|
|
49
|
+
return { type: 'tarball', url: cleanSource, subdir };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cleanSource.endsWith('.zip')) {
|
|
53
|
+
return { type: 'zip', url: cleanSource, subdir };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let gitUrl = cleanSource;
|
|
57
|
+
if (gitUrl.startsWith('git@') || gitUrl.startsWith('ext::')) {
|
|
58
|
+
throw new Error('Only HTTPS git URLs are allowed for strategy install');
|
|
59
|
+
}
|
|
60
|
+
if (!gitUrl.startsWith('http://') && !gitUrl.startsWith('https://')) {
|
|
61
|
+
gitUrl = `https://${gitUrl}`;
|
|
62
|
+
}
|
|
63
|
+
if (gitUrl.startsWith('http://')) {
|
|
64
|
+
throw new Error('Only HTTPS sources are allowed for strategy install');
|
|
65
|
+
}
|
|
66
|
+
if (!gitUrl.endsWith('.git')) {
|
|
67
|
+
gitUrl = `${gitUrl}.git`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { type: 'git', url: gitUrl, subdir };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hashContent(input: string): string {
|
|
74
|
+
return createHash('sha256').update(input).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function copyDirSync(src: string, dest: string): void {
|
|
78
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
79
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
80
|
+
const srcPath = path.join(src, entry.name);
|
|
81
|
+
const destPath = path.join(dest, entry.name);
|
|
82
|
+
if (entry.isSymbolicLink()) {
|
|
83
|
+
const target = fs.readlinkSync(srcPath);
|
|
84
|
+
fs.symlinkSync(target, destPath);
|
|
85
|
+
} else if (entry.isDirectory()) {
|
|
86
|
+
copyDirSync(srcPath, destPath);
|
|
87
|
+
} else if (entry.isFile()) {
|
|
88
|
+
fs.copyFileSync(srcPath, destPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function downloadFile(url: string, dest: string): void {
|
|
94
|
+
execFileSync('curl', ['-fsSL', '--proto', '=https', '-o', dest, url], {
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
timeout: 60_000,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findArchiveRoot(dir: string): string {
|
|
101
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
102
|
+
const dirs = entries.filter((entry) => entry.isDirectory());
|
|
103
|
+
if (dirs.length === 1 && !fs.existsSync(path.join(dir, 'app.md'))) {
|
|
104
|
+
return path.join(dir, dirs[0].name);
|
|
105
|
+
}
|
|
106
|
+
return dir;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function deriveAllowedHosts(explicit: string[] | undefined, sources: SourceDef[]): string[] {
|
|
110
|
+
const hosts = new Set(explicit || []);
|
|
111
|
+
for (const source of sources) {
|
|
112
|
+
if (source.url.startsWith('/') || source.url.includes('${')) continue;
|
|
113
|
+
try {
|
|
114
|
+
hosts.add(new URL(source.url).hostname);
|
|
115
|
+
} catch {
|
|
116
|
+
// Ignore unparseable URLs here; validated elsewhere.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return hosts.size > 0 ? Array.from(hosts) : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveAllowedHosts(explicit: string[] | undefined, sources: SourceDef[]): string[] {
|
|
123
|
+
const normalizedExplicit = Array.isArray(explicit)
|
|
124
|
+
? explicit.map((host) => host.trim()).filter((host) => host.length > 0)
|
|
125
|
+
: undefined;
|
|
126
|
+
if (normalizedExplicit && normalizedExplicit.length > 0) {
|
|
127
|
+
return deriveAllowedHosts(normalizedExplicit, sources);
|
|
128
|
+
}
|
|
129
|
+
return deriveAllowedHosts(undefined, sources);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseAppMarkdownManifest(appRoot: string, strategyId: string): { manifest: StrategyManifest; signaturePresent: boolean } {
|
|
133
|
+
const mdPath = path.join(appRoot, 'app.md');
|
|
134
|
+
if (!fs.existsSync(mdPath)) {
|
|
135
|
+
throw new Error('Source package missing app.md');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const raw = fs.readFileSync(mdPath, 'utf8');
|
|
139
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
140
|
+
if (!match) {
|
|
141
|
+
throw new Error('app.md missing YAML frontmatter');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const parsed = parseYaml(match[1]) as Record<string, unknown>;
|
|
145
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
146
|
+
throw new Error('app.md frontmatter is invalid');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const sources = (parsed.sources as SourceDef[]) || [];
|
|
150
|
+
const manifest: StrategyManifest = {
|
|
151
|
+
id: strategyId,
|
|
152
|
+
name: (parsed.name as string) || strategyId,
|
|
153
|
+
icon: parsed.icon as string | undefined,
|
|
154
|
+
category: parsed.category as string | undefined,
|
|
155
|
+
size: parsed.size as string | undefined,
|
|
156
|
+
autoStart: parsed.autoStart === true,
|
|
157
|
+
ticker: parsed.ticker as StrategyManifest['ticker'],
|
|
158
|
+
jobs: parsed.jobs as StrategyManifest['jobs'],
|
|
159
|
+
sources,
|
|
160
|
+
keys: parsed.keys as StrategyManifest['keys'],
|
|
161
|
+
hooks: (parsed.hooks as StrategyManifest['hooks']) || {},
|
|
162
|
+
config: (parsed.config as StrategyManifest['config']) || {},
|
|
163
|
+
permissions: (parsed.permissions as string[]) || [],
|
|
164
|
+
limits: parsed.limits as StrategyManifest['limits'],
|
|
165
|
+
allowedHosts: resolveAllowedHosts(parsed.allowedHosts as string[] | undefined, sources),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
manifest.sources = orderSources(manifest.sources);
|
|
169
|
+
|
|
170
|
+
const errors = validateManifest(manifest);
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
throw new Error(`Invalid strategy manifest: ${errors.join('; ')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const signaturePresent = Boolean(parsed.signature)
|
|
176
|
+
|| fs.existsSync(path.join(appRoot, 'SIGNATURE'))
|
|
177
|
+
|| fs.existsSync(path.join(appRoot, 'app.md.sig'))
|
|
178
|
+
|| fs.existsSync(path.join(appRoot, 'manifest.sig'));
|
|
179
|
+
|
|
180
|
+
return { manifest, signaturePresent };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function fetchSourceToTemp(parsed: ParsedSource, tempDir: string): Promise<{ rootDir: string; ref: string | null }> {
|
|
184
|
+
if (parsed.type === 'local') {
|
|
185
|
+
const absPath = path.resolve(parsed.url);
|
|
186
|
+
if (!fs.existsSync(absPath)) throw new Error(`Local path not found: ${absPath}`);
|
|
187
|
+
copyDirSync(absPath, tempDir);
|
|
188
|
+
return { rootDir: tempDir, ref: null };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (parsed.type === 'git') {
|
|
192
|
+
await validateExternalUrl(parsed.url);
|
|
193
|
+
execFileSync('git', ['clone', '--depth', '1', parsed.url, tempDir], {
|
|
194
|
+
stdio: 'pipe',
|
|
195
|
+
timeout: 60_000,
|
|
196
|
+
});
|
|
197
|
+
const ref = execFileSync('git', ['-C', tempDir, 'rev-parse', 'HEAD'], {
|
|
198
|
+
stdio: 'pipe',
|
|
199
|
+
timeout: 10_000,
|
|
200
|
+
}).toString().trim() || null;
|
|
201
|
+
return { rootDir: tempDir, ref };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (parsed.type === 'tarball') {
|
|
205
|
+
const archivePath = path.join(tempDir, 'archive.tar.gz');
|
|
206
|
+
await validateExternalUrl(parsed.url);
|
|
207
|
+
downloadFile(parsed.url, archivePath);
|
|
208
|
+
const extractDir = path.join(tempDir, 'extracted');
|
|
209
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
210
|
+
execFileSync('tar', ['xzf', archivePath, '-C', extractDir], {
|
|
211
|
+
stdio: 'pipe',
|
|
212
|
+
timeout: 30_000,
|
|
213
|
+
});
|
|
214
|
+
return { rootDir: findArchiveRoot(extractDir), ref: null };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const archivePath = path.join(tempDir, 'archive.zip');
|
|
218
|
+
await validateExternalUrl(parsed.url);
|
|
219
|
+
downloadFile(parsed.url, archivePath);
|
|
220
|
+
const extractDir = path.join(tempDir, 'extracted');
|
|
221
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
222
|
+
execFileSync('unzip', ['-o', archivePath, '-d', extractDir], {
|
|
223
|
+
stdio: 'pipe',
|
|
224
|
+
timeout: 30_000,
|
|
225
|
+
});
|
|
226
|
+
return { rootDir: findArchiveRoot(extractDir), ref: null };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function applySubdir(rootDir: string, subdir: string | null): string {
|
|
230
|
+
if (!subdir) return rootDir;
|
|
231
|
+
const subPath = path.join(rootDir, subdir);
|
|
232
|
+
if (!fs.existsSync(subPath)) {
|
|
233
|
+
throw new Error(`Subdirectory not found: ${subdir}`);
|
|
234
|
+
}
|
|
235
|
+
return subPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function prepareThirdPartyStrategyFromSource(input: {
|
|
239
|
+
source: string;
|
|
240
|
+
strategyId: string;
|
|
241
|
+
strategyName?: string;
|
|
242
|
+
}): Promise<PreparedThirdPartyStrategy> {
|
|
243
|
+
const parsed = parseSource(input.source);
|
|
244
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aura-strategy-'));
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const { rootDir, ref } = await fetchSourceToTemp(parsed, tempDir);
|
|
248
|
+
const strategyRoot = applySubdir(rootDir, parsed.subdir);
|
|
249
|
+
const { manifest, signaturePresent } = parseAppMarkdownManifest(strategyRoot, input.strategyId);
|
|
250
|
+
if (input.strategyName && input.strategyName.trim()) {
|
|
251
|
+
manifest.name = input.strategyName.trim();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const normalizedUrl = parsed.type === 'local' ? path.resolve(parsed.url) : parsed.url;
|
|
255
|
+
const hash = hashContent(JSON.stringify(manifest));
|
|
256
|
+
const provenance: StrategyInstallProvenance = {
|
|
257
|
+
sourceType: parsed.type,
|
|
258
|
+
sourceUrl: normalizedUrl,
|
|
259
|
+
ref,
|
|
260
|
+
subdir: parsed.subdir,
|
|
261
|
+
hash,
|
|
262
|
+
signaturePresent,
|
|
263
|
+
installedAt: new Date().toISOString(),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return { manifest, provenance };
|
|
267
|
+
} finally {
|
|
268
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function prepareThirdPartyStrategyFromManifest(input: {
|
|
273
|
+
manifest: StrategyManifest;
|
|
274
|
+
strategyId: string;
|
|
275
|
+
sourceLabel?: string;
|
|
276
|
+
}): PreparedThirdPartyStrategy {
|
|
277
|
+
const manifest = {
|
|
278
|
+
...input.manifest,
|
|
279
|
+
id: input.strategyId,
|
|
280
|
+
name: input.manifest.name || input.strategyId,
|
|
281
|
+
hooks: input.manifest.hooks || {},
|
|
282
|
+
sources: orderSources(input.manifest.sources || []),
|
|
283
|
+
config: input.manifest.config || {},
|
|
284
|
+
permissions: input.manifest.permissions || [],
|
|
285
|
+
allowedHosts: resolveAllowedHosts(input.manifest.allowedHosts, input.manifest.sources || []),
|
|
286
|
+
} as StrategyManifest;
|
|
287
|
+
|
|
288
|
+
const errors = validateManifest(manifest);
|
|
289
|
+
if (errors.length > 0) {
|
|
290
|
+
throw new Error(`Invalid strategy manifest: ${errors.join('; ')}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const hash = hashContent(JSON.stringify(manifest));
|
|
294
|
+
const provenance: StrategyInstallProvenance = {
|
|
295
|
+
sourceType: 'inline',
|
|
296
|
+
sourceUrl: input.sourceLabel || 'inline-manifest',
|
|
297
|
+
ref: null,
|
|
298
|
+
subdir: null,
|
|
299
|
+
hash,
|
|
300
|
+
signaturePresent: false,
|
|
301
|
+
installedAt: new Date().toISOString(),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return { manifest, provenance };
|
|
305
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Manifest Loader
|
|
3
|
+
*
|
|
4
|
+
* Scans apps directory for strategy manifests (those with ticker or jobs field).
|
|
5
|
+
* Uses the 'yaml' package for full YAML parsing (nested objects, multi-line strings).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { parse as parseYaml } from 'yaml';
|
|
11
|
+
import { StrategyManifest, SourceDef, TickTier, TICK_INTERVALS } from './types';
|
|
12
|
+
import { isIPv4, isIPv6 } from 'net';
|
|
13
|
+
import { isPrivateIp } from '../network';
|
|
14
|
+
|
|
15
|
+
const VALID_TICKERS = Object.keys(TICK_INTERVALS) as TickTier[];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load all strategy manifests from apps/ directory.
|
|
19
|
+
* Only returns manifests with a ticker or jobs field (skips regular apps).
|
|
20
|
+
*/
|
|
21
|
+
export function loadStrategyManifests(): StrategyManifest[] {
|
|
22
|
+
const appsDir = path.join(process.cwd(), 'apps');
|
|
23
|
+
if (!fs.existsSync(appsDir)) return [];
|
|
24
|
+
|
|
25
|
+
const strategies: StrategyManifest[] = [];
|
|
26
|
+
|
|
27
|
+
for (const entry of fs.readdirSync(appsDir, { withFileTypes: true })) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
const mdPath = path.join(appsDir, entry.name, 'app.md');
|
|
30
|
+
if (!fs.existsSync(mdPath)) continue;
|
|
31
|
+
|
|
32
|
+
const raw = fs.readFileSync(mdPath, 'utf-8');
|
|
33
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
|
|
36
|
+
let manifest: Record<string, unknown>;
|
|
37
|
+
try {
|
|
38
|
+
manifest = parseYaml(match[1]);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(`[strategy] Failed to parse ${mdPath}:`, err);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hooks = manifest.hooks as StrategyManifest['hooks'] | undefined;
|
|
45
|
+
if (!manifest || (!manifest.ticker && !manifest.jobs && !hooks?.message)) continue;
|
|
46
|
+
|
|
47
|
+
const strategy: StrategyManifest = {
|
|
48
|
+
id: entry.name,
|
|
49
|
+
name: (manifest.name as string) || entry.name,
|
|
50
|
+
icon: manifest.icon as string | undefined,
|
|
51
|
+
category: manifest.category as string | undefined,
|
|
52
|
+
size: manifest.size as string | undefined,
|
|
53
|
+
autoStart: manifest.autoStart === true,
|
|
54
|
+
ticker: manifest.ticker as TickTier | undefined,
|
|
55
|
+
jobs: manifest.jobs as StrategyManifest['jobs'],
|
|
56
|
+
sources: (manifest.sources as SourceDef[]) || [],
|
|
57
|
+
keys: manifest.keys as StrategyManifest['keys'],
|
|
58
|
+
hooks: manifest.hooks as StrategyManifest['hooks'] || { tick: '', execute: '' },
|
|
59
|
+
config: (manifest.config as StrategyManifest['config']) || {},
|
|
60
|
+
permissions: (manifest.permissions as string[]) || [],
|
|
61
|
+
limits: manifest.limits as StrategyManifest['limits'],
|
|
62
|
+
allowedHosts: deriveAllowedHosts(
|
|
63
|
+
manifest.allowedHosts as string[] | undefined,
|
|
64
|
+
(manifest.sources as SourceDef[]) || [],
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const errors = validateManifest(strategy);
|
|
69
|
+
if (errors.length > 0) {
|
|
70
|
+
console.error(`[strategy] Invalid manifest ${entry.name}:`, errors);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Sort sources by dependency order
|
|
75
|
+
strategy.sources = orderSources(strategy.sources);
|
|
76
|
+
|
|
77
|
+
strategies.push(strategy);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return strategies;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate a strategy manifest. Returns array of error strings (empty = valid).
|
|
85
|
+
*/
|
|
86
|
+
export function validateManifest(manifest: StrategyManifest): string[] {
|
|
87
|
+
const errors: string[] = [];
|
|
88
|
+
|
|
89
|
+
const hasTicker = !!manifest.ticker || !!manifest.jobs;
|
|
90
|
+
const hasMessage = !!manifest.hooks?.message;
|
|
91
|
+
|
|
92
|
+
// hooks.tick is required only when ticker or jobs is set
|
|
93
|
+
if (hasTicker && !manifest.hooks?.tick) {
|
|
94
|
+
errors.push('Missing hooks.tick (required when ticker or jobs is set)');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (manifest.ticker && !VALID_TICKERS.includes(manifest.ticker)) {
|
|
98
|
+
errors.push(`Invalid ticker "${manifest.ticker}". Must be one of: ${VALID_TICKERS.join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (manifest.jobs) {
|
|
102
|
+
for (const job of manifest.jobs) {
|
|
103
|
+
if (!job.id) errors.push('Job missing id');
|
|
104
|
+
if (!job.ticker || !VALID_TICKERS.includes(job.ticker)) {
|
|
105
|
+
errors.push(`Job "${job.id}" has invalid ticker "${job.ticker}"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!hasTicker && !hasMessage) {
|
|
111
|
+
errors.push('Must have either ticker, jobs, or hooks.message');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Validate allowedHosts — reject private IPs/hosts
|
|
115
|
+
if (manifest.allowedHosts) {
|
|
116
|
+
for (const host of manifest.allowedHosts) {
|
|
117
|
+
if (isPrivateHost(host)) {
|
|
118
|
+
errors.push(`allowedHosts contains private/reserved host: "${host}"`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate source URLs — reject private IPs/hosts at load time
|
|
124
|
+
for (const source of manifest.sources) {
|
|
125
|
+
if (!source.url.startsWith('/') && !source.url.includes('${')) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(source.url);
|
|
128
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
129
|
+
errors.push(`Source "${source.id}" has private/reserved host in URL: "${parsed.hostname}"`);
|
|
130
|
+
}
|
|
131
|
+
if (manifest.allowedHosts && manifest.allowedHosts.length > 0 && !manifest.allowedHosts.includes(parsed.hostname)) {
|
|
132
|
+
errors.push(`Source "${source.id}" host "${parsed.hostname}" is not listed in allowedHosts`);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// URL with template vars or invalid — validated at fetch time
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!source.url.startsWith('/') && source.url.includes('${') && (!manifest.allowedHosts || manifest.allowedHosts.length === 0)) {
|
|
139
|
+
errors.push(`Source "${source.id}" uses a templated external URL and requires explicit allowedHosts`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validate source dependencies exist
|
|
144
|
+
const sourceIds = new Set(manifest.sources.map(s => s.id));
|
|
145
|
+
for (const source of manifest.sources) {
|
|
146
|
+
if (source.depends && !sourceIds.has(source.depends)) {
|
|
147
|
+
errors.push(`Source "${source.id}" depends on unknown source "${source.depends}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for circular dependencies
|
|
152
|
+
if (hasCircularDeps(manifest.sources)) {
|
|
153
|
+
errors.push('Circular source dependencies detected');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return errors;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Topological sort of sources by dependency order.
|
|
161
|
+
* Independent sources come first, dependent sources come after their parents.
|
|
162
|
+
*/
|
|
163
|
+
export function orderSources(sources: SourceDef[]): SourceDef[] {
|
|
164
|
+
if (sources.length === 0) return [];
|
|
165
|
+
|
|
166
|
+
const byId = new Map(sources.map(s => [s.id, s]));
|
|
167
|
+
const ordered: SourceDef[] = [];
|
|
168
|
+
const visited = new Set<string>();
|
|
169
|
+
const visiting = new Set<string>();
|
|
170
|
+
|
|
171
|
+
function visit(id: string) {
|
|
172
|
+
if (visited.has(id)) return;
|
|
173
|
+
if (visiting.has(id)) return; // circular — already caught by validation
|
|
174
|
+
visiting.add(id);
|
|
175
|
+
|
|
176
|
+
const source = byId.get(id);
|
|
177
|
+
if (!source) return;
|
|
178
|
+
|
|
179
|
+
if (source.depends) {
|
|
180
|
+
visit(source.depends);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
visiting.delete(id);
|
|
184
|
+
visited.add(id);
|
|
185
|
+
ordered.push(source);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const source of sources) {
|
|
189
|
+
visit(source.id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return ordered;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Derive the combined allowedHosts list from explicit YAML declaration
|
|
197
|
+
* and auto-extracted hostnames from source URLs.
|
|
198
|
+
*/
|
|
199
|
+
function deriveAllowedHosts(
|
|
200
|
+
explicit: string[] | undefined,
|
|
201
|
+
sources: SourceDef[],
|
|
202
|
+
): string[] {
|
|
203
|
+
const hosts = new Set(explicit || []);
|
|
204
|
+
|
|
205
|
+
for (const source of sources) {
|
|
206
|
+
// Skip internal URLs (start with /) and URLs with template vars
|
|
207
|
+
if (source.url.startsWith('/') || source.url.includes('${')) continue;
|
|
208
|
+
try {
|
|
209
|
+
const parsed = new URL(source.url);
|
|
210
|
+
hosts.add(parsed.hostname);
|
|
211
|
+
} catch {
|
|
212
|
+
// Skip unparseable URLs
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return hosts.size > 0 ? Array.from(hosts) : [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if a hostname string is obviously private (pre-DNS, for manifest validation).
|
|
221
|
+
*/
|
|
222
|
+
function isPrivateHost(hostname: string): boolean {
|
|
223
|
+
if (['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]', ''].includes(hostname)) return true;
|
|
224
|
+
if (isIPv4(hostname) || isIPv6(hostname)) return isPrivateIp(hostname);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check for circular dependencies in sources.
|
|
230
|
+
*/
|
|
231
|
+
function hasCircularDeps(sources: SourceDef[]): boolean {
|
|
232
|
+
const byId = new Map(sources.map(s => [s.id, s]));
|
|
233
|
+
const visited = new Set<string>();
|
|
234
|
+
const visiting = new Set<string>();
|
|
235
|
+
|
|
236
|
+
function hasCycle(id: string): boolean {
|
|
237
|
+
if (visiting.has(id)) return true;
|
|
238
|
+
if (visited.has(id)) return false;
|
|
239
|
+
visiting.add(id);
|
|
240
|
+
|
|
241
|
+
const source = byId.get(id);
|
|
242
|
+
if (source?.depends) {
|
|
243
|
+
if (hasCycle(source.depends)) return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
visiting.delete(id);
|
|
247
|
+
visited.add(id);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const source of sources) {
|
|
252
|
+
if (hasCycle(source.id)) return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return false;
|
|
256
|
+
}
|