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,1194 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
export type RuntimeMode = 'interactive_local' | 'headless_local' | 'remote';
|
|
4
|
+
export type AuthProvider = 'in_memory' | 'unix_socket' | 'keychain' | 'env' | 'interactive_auth' | 'pairing';
|
|
5
|
+
|
|
6
|
+
export const PROVIDER_ORDER: Record<RuntimeMode, AuthProvider[]> = {
|
|
7
|
+
interactive_local: ['in_memory', 'unix_socket', 'keychain', 'env', 'interactive_auth', 'pairing'],
|
|
8
|
+
headless_local: ['in_memory', 'unix_socket', 'keychain', 'env', 'pairing'],
|
|
9
|
+
remote: ['in_memory', 'keychain', 'env', 'pairing'],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function resolveProviderOrder(mode: RuntimeMode): AuthProvider[] {
|
|
13
|
+
return [...PROVIDER_ORDER[mode]];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const KEY_PART = /^[a-z0-9._-]+$/;
|
|
17
|
+
const SCOPE_VALUES = new Set(['default', 'read', 'write', 'admin']);
|
|
18
|
+
|
|
19
|
+
export function normalizeKeyPart(raw: string): string {
|
|
20
|
+
return raw.trim().toLowerCase().replace(/\s+/g, '-');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function toCanonicalKeychainKey(profile: string, agentId: string, scope: string): string {
|
|
24
|
+
const normalizedProfile = normalizeKeyPart(profile);
|
|
25
|
+
const normalizedAgentId = normalizeKeyPart(agentId);
|
|
26
|
+
const normalizedScope = normalizeKeyPart(scope);
|
|
27
|
+
|
|
28
|
+
if (!KEY_PART.test(normalizedProfile) || normalizedProfile.length < 1 || normalizedProfile.length > 32) {
|
|
29
|
+
throw new Error('CONFIG: invalid profile');
|
|
30
|
+
}
|
|
31
|
+
if (!KEY_PART.test(normalizedAgentId) || normalizedAgentId.length < 1 || normalizedAgentId.length > 64) {
|
|
32
|
+
throw new Error('CONFIG: invalid agentId');
|
|
33
|
+
}
|
|
34
|
+
if (!SCOPE_VALUES.has(normalizedScope)) {
|
|
35
|
+
throw new Error('CONFIG: invalid scope');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const key = `aurawallet:${normalizedProfile}:${normalizedAgentId}:${normalizedScope}`;
|
|
39
|
+
if (key.length > 160) {
|
|
40
|
+
throw new Error('CONFIG: keychain identifier too long');
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function legacyKeychainAliases(profile: string, agentId: string): string[] {
|
|
46
|
+
const normalizedProfile = normalizeKeyPart(profile);
|
|
47
|
+
const normalizedAgentId = normalizeKeyPart(agentId);
|
|
48
|
+
return [
|
|
49
|
+
`aura:${normalizedProfile}:${normalizedAgentId}`,
|
|
50
|
+
`aurawallet:${normalizedProfile}:${normalizedAgentId}`,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const REFRESH_BACKOFF_SECONDS = [2, 4, 8, 16, 32, 60, 120] as const;
|
|
55
|
+
|
|
56
|
+
export type RefreshFailure = 'INVALID' | 'DENIED' | 'REVOKED' | 'NETWORK' | 'UNAVAILABLE' | 'UNKNOWN';
|
|
57
|
+
export type RefreshState = 'active' | 'needs_reauth';
|
|
58
|
+
|
|
59
|
+
export function refreshAtMs(expMs: number, nowMs: number, jitterMs = 0): number {
|
|
60
|
+
const scheduled = expMs - 60_000 + jitterMs;
|
|
61
|
+
return Math.max(scheduled, nowMs);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function nextBackoffSeconds(attempt: number): number {
|
|
65
|
+
const boundedIndex = Math.max(0, Math.min(attempt, REFRESH_BACKOFF_SECONDS.length - 1));
|
|
66
|
+
return REFRESH_BACKOFF_SECONDS[boundedIndex];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isPermanentRefreshFailure(reason: RefreshFailure): boolean {
|
|
70
|
+
return reason === 'INVALID' || reason === 'DENIED' || reason === 'REVOKED';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveRefreshState(reason: RefreshFailure, nowMs: number, expMs: number): RefreshState {
|
|
74
|
+
if (isPermanentRefreshFailure(reason)) return 'needs_reauth';
|
|
75
|
+
if (nowMs >= expMs) return 'needs_reauth';
|
|
76
|
+
return 'active';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const PAIRING_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
80
|
+
const CODE_LENGTH = 16;
|
|
81
|
+
const CODE_BITS = CODE_LENGTH * 5; // base32 chars => 5 bits each
|
|
82
|
+
|
|
83
|
+
export type PairingErrorCode =
|
|
84
|
+
| 'PAIRING_EXPIRED'
|
|
85
|
+
| 'PAIRING_CONSUMED'
|
|
86
|
+
| 'PAIRING_ATTEMPTS_EXCEEDED'
|
|
87
|
+
| 'PAIRING_LOCKED'
|
|
88
|
+
| 'PAIRING_INVALID';
|
|
89
|
+
|
|
90
|
+
export interface PairingRecord {
|
|
91
|
+
pairingId: string;
|
|
92
|
+
issuedAt: number;
|
|
93
|
+
consumedAt?: number;
|
|
94
|
+
consumerFingerprint?: string;
|
|
95
|
+
nonce: string;
|
|
96
|
+
ttlMs: number;
|
|
97
|
+
failedAttempts: number;
|
|
98
|
+
sourceFailedAttempts: number;
|
|
99
|
+
sourceLockUntil?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function generatePairingCode(): string {
|
|
103
|
+
const bytes = randomBytes(CODE_LENGTH);
|
|
104
|
+
let out = '';
|
|
105
|
+
for (let i = 0; i < CODE_LENGTH; i++) {
|
|
106
|
+
out += PAIRING_ALPHABET[bytes[i] % PAIRING_ALPHABET.length];
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validatePairingCodeShape(code: string): boolean {
|
|
112
|
+
return new RegExp(`^[${PAIRING_ALPHABET}]{${CODE_LENGTH}}$`).test(code);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function assertPairingUsable(record: PairingRecord, nowMs: number): PairingErrorCode | null {
|
|
116
|
+
if (record.sourceLockUntil && nowMs < record.sourceLockUntil) return 'PAIRING_LOCKED';
|
|
117
|
+
if (nowMs > record.issuedAt + Math.min(record.ttlMs, 5 * 60_000)) return 'PAIRING_EXPIRED';
|
|
118
|
+
if (record.consumedAt) return 'PAIRING_CONSUMED';
|
|
119
|
+
if (record.failedAttempts >= 5 || record.sourceFailedAttempts >= 20) return 'PAIRING_ATTEMPTS_EXCEEDED';
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type RevocationScope = 'pairing' | 'agent_identity' | 'fingerprint';
|
|
124
|
+
|
|
125
|
+
export interface RevocationIndexRow {
|
|
126
|
+
tokenId: string;
|
|
127
|
+
pairingId: string;
|
|
128
|
+
agentId: string;
|
|
129
|
+
pubkeyFingerprint: string;
|
|
130
|
+
issuedAt: number;
|
|
131
|
+
revokedAt?: number;
|
|
132
|
+
revocationScope?: RevocationScope;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function matchesRevocationScope(row: RevocationIndexRow, scope: RevocationScope, value: string): boolean {
|
|
136
|
+
if (scope === 'pairing') return row.pairingId === value;
|
|
137
|
+
if (scope === 'agent_identity') return row.agentId === value;
|
|
138
|
+
return row.pubkeyFingerprint === value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const JWT_LIKE = /\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
|
|
142
|
+
const LONG_HEX = /\b(?:[A-Fa-f0-9]{24,}|[A-Za-z0-9+/]{24,}={0,2})\b/g;
|
|
143
|
+
const LONG_BLOB = /\b[A-Za-z0-9+/=]{96,}\b/g;
|
|
144
|
+
|
|
145
|
+
export type RedactionBucket = 'short' | 'medium' | 'long';
|
|
146
|
+
|
|
147
|
+
function bucketForLength(len: number): RedactionBucket {
|
|
148
|
+
if (len < 64) return 'short';
|
|
149
|
+
if (len < 256) return 'medium';
|
|
150
|
+
return 'long';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function redactSensitiveText(input: string): { redacted: string; buckets: RedactionBucket[] } {
|
|
154
|
+
const buckets: RedactionBucket[] = [];
|
|
155
|
+
|
|
156
|
+
const redact = (source: string, pattern: RegExp, label: string): string => source.replace(pattern, (match) => {
|
|
157
|
+
buckets.push(bucketForLength(match.length));
|
|
158
|
+
return `[REDACTED:${label}]`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
let output = input;
|
|
162
|
+
output = redact(output, JWT_LIKE, 'jwt');
|
|
163
|
+
output = redact(output, LONG_BLOB, 'blob');
|
|
164
|
+
output = redact(output, LONG_HEX, 'token');
|
|
165
|
+
|
|
166
|
+
return { redacted: output, buckets };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Loop 3 frozen contracts: agent identity, remote policy, reset controls, error envelope, telemetry schema.
|
|
170
|
+
export interface AgentIdentityMeta {
|
|
171
|
+
createdAt: string;
|
|
172
|
+
fingerprintPrefix: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface CanonicalAgentIdentity {
|
|
176
|
+
serviceSlug: string;
|
|
177
|
+
profileSlug: string;
|
|
178
|
+
agentId: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const REMOTE_PRIVATE_NETWORK = /^(10\.|127\.|169\.254\.|172\.(1[6-9]|2\d|3[0-1])\.|192\.168\.|::1$|fc|fd)/i;
|
|
182
|
+
|
|
183
|
+
function normalizeIdentitySegment(raw: string): string {
|
|
184
|
+
const normalized = raw
|
|
185
|
+
.normalize('NFKC')
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
189
|
+
.replace(/-+/g, '-')
|
|
190
|
+
.replace(/^-|-$/g, '')
|
|
191
|
+
.slice(0, 48);
|
|
192
|
+
if (!normalized) throw new Error('CONFIG_INVALID: empty normalized identity segment');
|
|
193
|
+
return normalized;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function canonicalizeAgentIdentity(serviceName: string, profile: string): CanonicalAgentIdentity {
|
|
197
|
+
const serviceSlug = normalizeIdentitySegment(serviceName);
|
|
198
|
+
const profileSlug = normalizeIdentitySegment(profile);
|
|
199
|
+
const agentId = `agent:${serviceSlug}:${profileSlug}`;
|
|
200
|
+
if (agentId.length > 120) throw new Error('CONFIG_INVALID: canonical agentId too long');
|
|
201
|
+
return { serviceSlug, profileSlug, agentId };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function assertNoAgentIdCollision(
|
|
205
|
+
candidate: CanonicalAgentIdentity,
|
|
206
|
+
existing: AgentIdentityMeta | null,
|
|
207
|
+
): void {
|
|
208
|
+
if (!existing) return;
|
|
209
|
+
throw new Error(
|
|
210
|
+
`ID_COLLISION: ${candidate.agentId} conflicts with createdAt=${existing.createdAt} fingerprint=${existing.fingerprintPrefix}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
215
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toOrigin(url: URL): string {
|
|
219
|
+
const port = url.port || (url.protocol === 'https:' ? '443' : url.protocol === 'http:' ? '80' : '');
|
|
220
|
+
return `${url.protocol}//${url.hostname}:${port}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function validateRemoteBootstrapEndpoint(
|
|
224
|
+
endpoint: string,
|
|
225
|
+
opts: { allowInsecureLocalHttp?: boolean; allowlistOrigins?: string[] } = {},
|
|
226
|
+
): { origin: string } {
|
|
227
|
+
const parsed = new URL(endpoint);
|
|
228
|
+
const isHttps = parsed.protocol === 'https:';
|
|
229
|
+
const loopback = isLoopbackHost(parsed.hostname);
|
|
230
|
+
|
|
231
|
+
if (!isHttps && !(parsed.protocol === 'http:' && loopback && opts.allowInsecureLocalHttp)) {
|
|
232
|
+
throw new Error('NETWORK_TLS');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const origin = toOrigin(parsed);
|
|
236
|
+
const normalizedAllowlist = new Set((opts.allowlistOrigins ?? []).map((item) => toOrigin(new URL(item))));
|
|
237
|
+
if (normalizedAllowlist.size > 0 && !normalizedAllowlist.has(origin)) {
|
|
238
|
+
throw new Error('REMOTE_ALLOWLIST_DENY');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!loopback && REMOTE_PRIVATE_NETWORK.test(parsed.hostname) && !normalizedAllowlist.has(origin)) {
|
|
242
|
+
throw new Error('REMOTE_ALLOWLIST_DENY');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { origin };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function assertNoRedirectStatus(statusCode: number): void {
|
|
249
|
+
if (statusCode >= 300 && statusCode < 400) {
|
|
250
|
+
throw new Error('REMOTE_REDIRECT_BLOCKED');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function assertResetConfirmation(input: {
|
|
255
|
+
interactive: boolean;
|
|
256
|
+
resetIdentity: boolean;
|
|
257
|
+
agentId: string;
|
|
258
|
+
typedConfirmation?: string;
|
|
259
|
+
confirmResetAgentId?: string;
|
|
260
|
+
}): void {
|
|
261
|
+
if (!input.resetIdentity) return;
|
|
262
|
+
if (input.interactive) {
|
|
263
|
+
if (input.typedConfirmation !== `RESET ${input.agentId}`) throw new Error('RESET_CONFIRM_REQUIRED');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (input.confirmResetAgentId !== input.agentId) throw new Error('RESET_CONFIRM_REQUIRED');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function isResetRateLimited(resetTimestampsMs: number[], nowMs: number): boolean {
|
|
270
|
+
const windowMs = 10 * 60_000;
|
|
271
|
+
const recent = resetTimestampsMs.filter((ts) => nowMs - ts <= windowMs);
|
|
272
|
+
return recent.length > 2;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const AUTH_ERROR_SCHEMA_VERSION = 'v1' as const;
|
|
276
|
+
|
|
277
|
+
export const AUTH_EXIT_CODES: Record<string, number> = {
|
|
278
|
+
CONFIG_INVALID: 31,
|
|
279
|
+
PAIRING_TIMEOUT: 41,
|
|
280
|
+
PAIRING_DENIED: 42,
|
|
281
|
+
PAIRING_REPLAY: 43,
|
|
282
|
+
PAIRING_EXPIRED: 44,
|
|
283
|
+
NETWORK_TLS: 51,
|
|
284
|
+
NETWORK_UNREACHABLE: 52,
|
|
285
|
+
REMOTE_ALLOWLIST_DENY: 53,
|
|
286
|
+
REMOTE_REDIRECT_BLOCKED: 54,
|
|
287
|
+
ID_COLLISION: 61,
|
|
288
|
+
FINGERPRINT_MISMATCH: 62,
|
|
289
|
+
RESET_CONFIRM_REQUIRED: 71,
|
|
290
|
+
RESET_RATE_LIMIT: 72,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export interface AuthErrorEnvelope {
|
|
294
|
+
authErrorSchemaVersion: typeof AUTH_ERROR_SCHEMA_VERSION;
|
|
295
|
+
family: string;
|
|
296
|
+
subcode: string;
|
|
297
|
+
exitCode: number;
|
|
298
|
+
message: string;
|
|
299
|
+
hint?: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function toAuthErrorEnvelope(params: {
|
|
303
|
+
family: string;
|
|
304
|
+
subcode: string;
|
|
305
|
+
message: string;
|
|
306
|
+
hint?: string;
|
|
307
|
+
}): AuthErrorEnvelope {
|
|
308
|
+
const exitCode = AUTH_EXIT_CODES[params.subcode] ?? 1;
|
|
309
|
+
return {
|
|
310
|
+
authErrorSchemaVersion: AUTH_ERROR_SCHEMA_VERSION,
|
|
311
|
+
family: params.family,
|
|
312
|
+
subcode: params.subcode,
|
|
313
|
+
exitCode,
|
|
314
|
+
message: params.message,
|
|
315
|
+
...(params.hint ? { hint: params.hint } : {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface RegisterTelemetryEvent {
|
|
320
|
+
event: string;
|
|
321
|
+
timestamp: string;
|
|
322
|
+
agentId: string;
|
|
323
|
+
profile: string;
|
|
324
|
+
scope: string;
|
|
325
|
+
authMode: RuntimeMode;
|
|
326
|
+
attempt: number;
|
|
327
|
+
durationMs: number;
|
|
328
|
+
result: 'success' | 'failure' | 'pending';
|
|
329
|
+
failureFamily: string | null;
|
|
330
|
+
failureSubcode: string | null;
|
|
331
|
+
persistenceBackend: 'keychain' | 'memory' | 'none';
|
|
332
|
+
providerPath: string[];
|
|
333
|
+
correlationId: string;
|
|
334
|
+
fingerprintPrefix?: string;
|
|
335
|
+
endpointOrigin?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function sanitizeRegisterTelemetryEvent(event: RegisterTelemetryEvent): RegisterTelemetryEvent {
|
|
339
|
+
if (!event.event || !event.agentId || !event.profile || !event.scope || !event.correlationId) {
|
|
340
|
+
throw new Error('CONFIG_INVALID: missing required telemetry fields');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
...event,
|
|
345
|
+
fingerprintPrefix: event.fingerprintPrefix?.slice(0, 8),
|
|
346
|
+
endpointOrigin: event.endpointOrigin ? new URL(event.endpointOrigin).origin : undefined,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const PAIRING_MIN_BITS = CODE_BITS;
|
|
351
|
+
export const PAIRING_MAX_TTL_MS = 5 * 60_000;
|
|
352
|
+
|
|
353
|
+
// Loop 4 frozen contracts: lifecycle/rotation outcomes, headless cooldown, lease contention,
|
|
354
|
+
// remediation compatibility, and TOCTOU-safe local trust-proof sequencing.
|
|
355
|
+
export type AuthLifecycleState = 'ACTIVE' | 'ROTATING' | 'DEGRADED' | 'NEEDS_REAUTH' | 'REVOKED';
|
|
356
|
+
|
|
357
|
+
export const DEFAULT_ROTATION_GRACE_SECONDS = 30;
|
|
358
|
+
export const MAX_ROTATION_GRACE_SECONDS = 120;
|
|
359
|
+
|
|
360
|
+
export function applyRotationGraceSeconds(requested?: number): number {
|
|
361
|
+
const value = requested ?? DEFAULT_ROTATION_GRACE_SECONDS;
|
|
362
|
+
if (!Number.isFinite(value) || value < 0 || value > MAX_ROTATION_GRACE_SECONDS) {
|
|
363
|
+
throw new Error('CONFIG: invalid rotation grace seconds');
|
|
364
|
+
}
|
|
365
|
+
return Math.trunc(value);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function enforceRotationOverlapInvariant(params: {
|
|
369
|
+
cutoverStartedAtMs: number;
|
|
370
|
+
nowMs: number;
|
|
371
|
+
graceSeconds: number;
|
|
372
|
+
}): { overlapWindowMs: number; overlapInvariantSatisfied: boolean; forceExpireOldToken: boolean } {
|
|
373
|
+
const graceSeconds = applyRotationGraceSeconds(params.graceSeconds);
|
|
374
|
+
const overlapWindowMs = Math.max(0, params.nowMs - params.cutoverStartedAtMs);
|
|
375
|
+
const capMs = graceSeconds * 1000;
|
|
376
|
+
const overlapInvariantSatisfied = overlapWindowMs <= capMs;
|
|
377
|
+
return {
|
|
378
|
+
overlapWindowMs,
|
|
379
|
+
overlapInvariantSatisfied,
|
|
380
|
+
forceExpireOldToken: !overlapInvariantSatisfied,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export type RotationOutcomeKey =
|
|
385
|
+
| 'PREFLIGHT_FAIL'
|
|
386
|
+
| 'CANDIDATE_MINT_FAIL'
|
|
387
|
+
| 'PROBE_RETRYABLE'
|
|
388
|
+
| 'PROBE_CEILING'
|
|
389
|
+
| 'COMMIT_PERSIST_FAIL'
|
|
390
|
+
| 'ACTIVATE_OK_REVOKE_PENDING'
|
|
391
|
+
| 'REVOKE_SUCCESS'
|
|
392
|
+
| 'REVOKE_FAIL_WITHIN_GRACE'
|
|
393
|
+
| 'REVOKE_UNRESOLVED_AT_DEADLINE'
|
|
394
|
+
| 'ROLLBACK_SUCCESS'
|
|
395
|
+
| 'ROLLBACK_FAIL'
|
|
396
|
+
| 'TIMEOUT_REVOKE_UNKNOWN';
|
|
397
|
+
|
|
398
|
+
export interface RotationOutcome {
|
|
399
|
+
oldValid: boolean;
|
|
400
|
+
newValid: boolean;
|
|
401
|
+
terminalState: AuthLifecycleState;
|
|
402
|
+
subcode: string;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export const ROTATION_OUTCOME_TABLE: Record<RotationOutcomeKey, RotationOutcome> = {
|
|
406
|
+
PREFLIGHT_FAIL: { oldValid: true, newValid: false, terminalState: 'ACTIVE', subcode: 'ROTATE_PREFLIGHT_FAILED' },
|
|
407
|
+
CANDIDATE_MINT_FAIL: { oldValid: true, newValid: false, terminalState: 'ACTIVE', subcode: 'ROTATE_ISSUE_FAILED' },
|
|
408
|
+
PROBE_RETRYABLE: { oldValid: true, newValid: false, terminalState: 'ROTATING', subcode: 'ROTATE_PROBE_RETRYABLE' },
|
|
409
|
+
PROBE_CEILING: { oldValid: true, newValid: false, terminalState: 'DEGRADED', subcode: 'ROTATE_PROBE_CEILING' },
|
|
410
|
+
COMMIT_PERSIST_FAIL: { oldValid: true, newValid: false, terminalState: 'DEGRADED', subcode: 'ROTATE_COMMIT_PERSIST_FAILED' },
|
|
411
|
+
ACTIVATE_OK_REVOKE_PENDING: { oldValid: true, newValid: true, terminalState: 'ROTATING', subcode: 'ROTATE_REVOKE_PENDING' },
|
|
412
|
+
REVOKE_SUCCESS: { oldValid: false, newValid: true, terminalState: 'ACTIVE', subcode: 'ROTATE_SUCCESS' },
|
|
413
|
+
REVOKE_FAIL_WITHIN_GRACE: { oldValid: true, newValid: true, terminalState: 'DEGRADED', subcode: 'ROTATE_REVOKE_FAILED' },
|
|
414
|
+
REVOKE_UNRESOLVED_AT_DEADLINE: { oldValid: false, newValid: true, terminalState: 'DEGRADED', subcode: 'ROTATE_OVERLAP_CAP_ENFORCED' },
|
|
415
|
+
ROLLBACK_SUCCESS: { oldValid: true, newValid: false, terminalState: 'ACTIVE', subcode: 'ROTATE_ROLLBACK_SUCCESS' },
|
|
416
|
+
ROLLBACK_FAIL: { oldValid: false, newValid: false, terminalState: 'NEEDS_REAUTH', subcode: 'ROTATE_ROLLBACK_FAILED' },
|
|
417
|
+
TIMEOUT_REVOKE_UNKNOWN: { oldValid: false, newValid: true, terminalState: 'DEGRADED', subcode: 'ROTATE_TIMEOUT_REVOKE_UNKNOWN' },
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export function resolveRotationOutcome(key: RotationOutcomeKey): RotationOutcome {
|
|
421
|
+
return ROTATION_OUTCOME_TABLE[key];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const HEADLESS_COOLDOWN_SECONDS = [60, 120, 300, 600] as const;
|
|
425
|
+
export const MAX_FAILED_EPISODES_PER_HOUR = 4;
|
|
426
|
+
|
|
427
|
+
export function cooldownSecondsForFailedEpisodes(failedEpisodes: number): number {
|
|
428
|
+
const idx = Math.max(0, Math.min(Math.max(1, failedEpisodes) - 1, HEADLESS_COOLDOWN_SECONDS.length - 1));
|
|
429
|
+
return HEADLESS_COOLDOWN_SECONDS[idx];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function stableJitterRatio(agentId: string): number {
|
|
433
|
+
let hash = 0;
|
|
434
|
+
for (let i = 0; i < agentId.length; i++) hash = (hash * 31 + agentId.charCodeAt(i)) | 0;
|
|
435
|
+
return (Math.abs(hash) % 401) / 1000 - 0.2; // [-0.2, +0.2]
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function cooldownWithDeterministicJitterSeconds(agentId: string, baseSeconds: number): number {
|
|
439
|
+
const adjusted = baseSeconds * (1 + stableJitterRatio(agentId));
|
|
440
|
+
return Math.max(1, Math.round(adjusted));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function nextHeadlessRenewalWindow(params: {
|
|
444
|
+
agentId: string;
|
|
445
|
+
nowMs: number;
|
|
446
|
+
failedEpisodesLastHour: number;
|
|
447
|
+
consecutiveFailedEpisodes: number;
|
|
448
|
+
}): { state: AuthLifecycleState; subcode: string; nextAllowedAttemptAt: string | null } {
|
|
449
|
+
if (params.failedEpisodesLastHour > MAX_FAILED_EPISODES_PER_HOUR) {
|
|
450
|
+
return {
|
|
451
|
+
state: 'NEEDS_REAUTH',
|
|
452
|
+
subcode: 'REAUTH_EPISODE_LIMIT_EXCEEDED',
|
|
453
|
+
nextAllowedAttemptAt: new Date(params.nowMs + 60 * 60_000).toISOString(),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const base = cooldownSecondsForFailedEpisodes(params.consecutiveFailedEpisodes);
|
|
458
|
+
const cooldownSeconds = cooldownWithDeterministicJitterSeconds(params.agentId, base);
|
|
459
|
+
return {
|
|
460
|
+
state: 'DEGRADED',
|
|
461
|
+
subcode: 'RENEWAL_COOLDOWN',
|
|
462
|
+
nextAllowedAttemptAt: new Date(params.nowMs + cooldownSeconds * 1000).toISOString(),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export interface AuthLeaseRecord {
|
|
467
|
+
ownerId: string;
|
|
468
|
+
leaseVersion: number;
|
|
469
|
+
acquiredAtMs: number;
|
|
470
|
+
lastRenewedAtMs: number;
|
|
471
|
+
heartbeatMisses: number;
|
|
472
|
+
ownerProofFailures: number;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export type LeaseStaleReason = 'HEARTBEAT_TIMEOUT' | 'OWNER_PROOF_FAILED';
|
|
476
|
+
|
|
477
|
+
export function leaseStaleReason(lease: AuthLeaseRecord, nowMs: number): LeaseStaleReason | null {
|
|
478
|
+
if (nowMs - lease.lastRenewedAtMs > 45_000 || lease.heartbeatMisses >= 2) return 'HEARTBEAT_TIMEOUT';
|
|
479
|
+
if (lease.ownerProofFailures >= 2) return 'OWNER_PROOF_FAILED';
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function attemptLeaseTakeover(params: {
|
|
484
|
+
currentLease: AuthLeaseRecord;
|
|
485
|
+
expectedLeaseVersion: number;
|
|
486
|
+
contenderOwnerId: string;
|
|
487
|
+
nowMs: number;
|
|
488
|
+
}): { takeoverSucceeded: boolean; newLease?: AuthLeaseRecord } {
|
|
489
|
+
if (params.currentLease.leaseVersion !== params.expectedLeaseVersion) {
|
|
490
|
+
return { takeoverSucceeded: false };
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
takeoverSucceeded: true,
|
|
494
|
+
newLease: {
|
|
495
|
+
ownerId: params.contenderOwnerId,
|
|
496
|
+
leaseVersion: params.currentLease.leaseVersion + 1,
|
|
497
|
+
acquiredAtMs: params.nowMs,
|
|
498
|
+
lastRenewedAtMs: params.nowMs,
|
|
499
|
+
heartbeatMisses: 0,
|
|
500
|
+
ownerProofFailures: 0,
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export const AUTH_REMEDIATION_SCHEMA_VERSION = 'v1' as const;
|
|
506
|
+
|
|
507
|
+
export interface AuthRemediationPayload {
|
|
508
|
+
authRemediationSchemaVersion: string;
|
|
509
|
+
subcode: string;
|
|
510
|
+
recommendedAction: string;
|
|
511
|
+
humanHint: string;
|
|
512
|
+
nextAllowedAttemptAt: string | null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const KNOWN_REMEDIATION_SUBCODES: Record<string, { action: string; hint: string }> = {
|
|
516
|
+
NETWORK_TLS: { action: 'verify_tls_configuration', hint: 'TLS required for remote auth endpoint.' },
|
|
517
|
+
REMOTE_ALLOWLIST_DENY: { action: 'update_allowlist', hint: 'Endpoint origin denied by remote allowlist.' },
|
|
518
|
+
RESET_CONFIRM_REQUIRED: { action: 'confirm_reset_identity', hint: 'Provide explicit reset confirmation to continue.' },
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export function toAuthRemediationPayload(subcode: string, nextAllowedAttemptAt: string | null): AuthRemediationPayload {
|
|
522
|
+
const known = KNOWN_REMEDIATION_SUBCODES[subcode];
|
|
523
|
+
if (!known) {
|
|
524
|
+
return {
|
|
525
|
+
authRemediationSchemaVersion: AUTH_REMEDIATION_SCHEMA_VERSION,
|
|
526
|
+
subcode,
|
|
527
|
+
recommendedAction: 'inspect_auth_logs',
|
|
528
|
+
humanHint: 'Unknown auth failure; inspect logs and re-run registration if needed.',
|
|
529
|
+
nextAllowedAttemptAt,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
authRemediationSchemaVersion: AUTH_REMEDIATION_SCHEMA_VERSION,
|
|
534
|
+
subcode,
|
|
535
|
+
recommendedAction: known.action,
|
|
536
|
+
humanHint: known.hint,
|
|
537
|
+
nextAllowedAttemptAt,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function assertSupportedRemediationSchema(version: string): void {
|
|
542
|
+
const major = version.match(/^v(\d+)/)?.[1];
|
|
543
|
+
if (!major || major !== '1') throw new Error('REMEDIATION_SCHEMA_UNSUPPORTED');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export type TrustPolicyMode = 'strict' | 'compatible';
|
|
547
|
+
export type TrustDecision = 'allow' | 'deny' | 'allow_with_warning';
|
|
548
|
+
export type TrustPolicyResult = 'pass' | 'fail' | 'warn' | 'not_checked';
|
|
549
|
+
|
|
550
|
+
export interface LocalTrustEvidence {
|
|
551
|
+
uid: number;
|
|
552
|
+
pid: number;
|
|
553
|
+
exePathPolicy: TrustPolicyResult;
|
|
554
|
+
exeHashPolicy: TrustPolicyResult;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export interface LocalTrustDecision {
|
|
558
|
+
trustDecision: TrustDecision;
|
|
559
|
+
uidMatch: boolean;
|
|
560
|
+
exePathPolicy: TrustPolicyResult;
|
|
561
|
+
exeHashPolicy: TrustPolicyResult;
|
|
562
|
+
policyMode: TrustPolicyMode;
|
|
563
|
+
decisionReasonCodes: string[];
|
|
564
|
+
evidenceStable: boolean;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function evaluateLocalTrustProofSequence(params: {
|
|
568
|
+
policyMode: TrustPolicyMode;
|
|
569
|
+
accepted: LocalTrustEvidence;
|
|
570
|
+
issueCheck: LocalTrustEvidence;
|
|
571
|
+
}): LocalTrustDecision {
|
|
572
|
+
const evidenceStable =
|
|
573
|
+
params.accepted.uid === params.issueCheck.uid
|
|
574
|
+
&& params.accepted.pid === params.issueCheck.pid
|
|
575
|
+
&& params.accepted.exePathPolicy === params.issueCheck.exePathPolicy
|
|
576
|
+
&& params.accepted.exeHashPolicy === params.issueCheck.exeHashPolicy;
|
|
577
|
+
|
|
578
|
+
if (!evidenceStable) throw new Error('LOCAL_TRUST_PROOF_CHANGED');
|
|
579
|
+
|
|
580
|
+
const uidMatch = params.accepted.uid === params.issueCheck.uid;
|
|
581
|
+
if (!uidMatch) {
|
|
582
|
+
return {
|
|
583
|
+
trustDecision: 'deny',
|
|
584
|
+
uidMatch: false,
|
|
585
|
+
exePathPolicy: params.issueCheck.exePathPolicy,
|
|
586
|
+
exeHashPolicy: params.issueCheck.exeHashPolicy,
|
|
587
|
+
policyMode: params.policyMode,
|
|
588
|
+
decisionReasonCodes: ['UID_MISMATCH'],
|
|
589
|
+
evidenceStable,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const strictFail = params.issueCheck.exePathPolicy === 'fail' || params.issueCheck.exeHashPolicy === 'fail';
|
|
594
|
+
if (params.policyMode === 'strict') {
|
|
595
|
+
return {
|
|
596
|
+
trustDecision: strictFail ? 'deny' : 'allow',
|
|
597
|
+
uidMatch: true,
|
|
598
|
+
exePathPolicy: params.issueCheck.exePathPolicy,
|
|
599
|
+
exeHashPolicy: params.issueCheck.exeHashPolicy,
|
|
600
|
+
policyMode: 'strict',
|
|
601
|
+
decisionReasonCodes: strictFail ? ['STRICT_POLICY_FAIL'] : ['STRICT_POLICY_PASS'],
|
|
602
|
+
evidenceStable,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const compatibleWarn = strictFail || params.issueCheck.exePathPolicy === 'warn' || params.issueCheck.exeHashPolicy === 'warn';
|
|
607
|
+
return {
|
|
608
|
+
trustDecision: compatibleWarn ? 'allow_with_warning' : 'allow',
|
|
609
|
+
uidMatch: true,
|
|
610
|
+
exePathPolicy: params.issueCheck.exePathPolicy,
|
|
611
|
+
exeHashPolicy: params.issueCheck.exeHashPolicy,
|
|
612
|
+
policyMode: 'compatible',
|
|
613
|
+
decisionReasonCodes: compatibleWarn ? ['COMPATIBLE_POLICY_WARNING'] : ['COMPATIBLE_POLICY_PASS'],
|
|
614
|
+
evidenceStable,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Loop 5 frozen contracts: rollout compatibility, promotion gate safety, break-glass constraints,
|
|
619
|
+
// conformance recency, and unknown-major schema handling.
|
|
620
|
+
export type RolloutPolicyMode = 'observe' | 'warn' | 'enforce';
|
|
621
|
+
export type CapabilityCriticality = 'critical' | 'non_critical';
|
|
622
|
+
|
|
623
|
+
export interface CapabilityRegistryEntry {
|
|
624
|
+
capabilityKey: string;
|
|
625
|
+
criticality: CapabilityCriticality;
|
|
626
|
+
owner: string;
|
|
627
|
+
introducedInContractVersion: string;
|
|
628
|
+
lastModifiedAt: string;
|
|
629
|
+
registryVersion: string;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export interface CapabilityNegotiationDecision {
|
|
633
|
+
allowed: boolean;
|
|
634
|
+
subcode: string | null;
|
|
635
|
+
missingClientCaps: string[];
|
|
636
|
+
missingServerCaps: string[];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function resolveCapabilityNegotiation(params: {
|
|
640
|
+
policyMode: RolloutPolicyMode;
|
|
641
|
+
requiredCaps: string[];
|
|
642
|
+
clientCaps: string[];
|
|
643
|
+
serverCaps: string[];
|
|
644
|
+
registry: Record<string, CapabilityRegistryEntry>;
|
|
645
|
+
}): CapabilityNegotiationDecision {
|
|
646
|
+
const required = [...new Set(params.requiredCaps)];
|
|
647
|
+
const clientSet = new Set(params.clientCaps);
|
|
648
|
+
const serverSet = new Set(params.serverCaps);
|
|
649
|
+
|
|
650
|
+
const missingServerCaps = required.filter((cap) => !serverSet.has(cap));
|
|
651
|
+
if (missingServerCaps.length > 0) {
|
|
652
|
+
return { allowed: false, subcode: 'SERVER_CAPABILITY_MISSING', missingClientCaps: [], missingServerCaps };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const missingClientCaps = required.filter((cap) => !clientSet.has(cap));
|
|
656
|
+
if (missingClientCaps.length === 0) {
|
|
657
|
+
return { allowed: true, subcode: null, missingClientCaps: [], missingServerCaps: [] };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const hasCriticalGap = missingClientCaps.some((cap) => (params.registry[cap]?.criticality ?? 'critical') === 'critical');
|
|
661
|
+
if (params.policyMode === 'enforce' || hasCriticalGap) {
|
|
662
|
+
return { allowed: false, subcode: 'CAPABILITY_MISMATCH', missingClientCaps, missingServerCaps: [] };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
allowed: true,
|
|
667
|
+
subcode: params.policyMode === 'warn' ? 'CAPABILITY_MISMATCH_WARN' : 'CAPABILITY_MISMATCH_OBSERVED',
|
|
668
|
+
missingClientCaps,
|
|
669
|
+
missingServerCaps: [],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export interface PromotionSampleFloor {
|
|
674
|
+
minHandshakeSamples: number;
|
|
675
|
+
minDistinctAgents: number;
|
|
676
|
+
minDistinctProfiles: number;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export const PROMOTION_SAMPLE_FLOORS: Record<'observe_to_warn' | 'warn_to_enforce', PromotionSampleFloor> = {
|
|
680
|
+
observe_to_warn: { minHandshakeSamples: 10_000, minDistinctAgents: 50, minDistinctProfiles: 3 },
|
|
681
|
+
warn_to_enforce: { minHandshakeSamples: 50_000, minDistinctAgents: 200, minDistinctProfiles: 5 },
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
export function hasPromotionSampleFloor(params: {
|
|
685
|
+
transition: keyof typeof PROMOTION_SAMPLE_FLOORS;
|
|
686
|
+
handshakeSamples: number;
|
|
687
|
+
distinctAgents: number;
|
|
688
|
+
distinctProfiles: number;
|
|
689
|
+
}): { pass: boolean; subcode: string | null } {
|
|
690
|
+
const floor = PROMOTION_SAMPLE_FLOORS[params.transition];
|
|
691
|
+
const pass =
|
|
692
|
+
params.handshakeSamples >= floor.minHandshakeSamples
|
|
693
|
+
&& params.distinctAgents >= floor.minDistinctAgents
|
|
694
|
+
&& params.distinctProfiles >= floor.minDistinctProfiles;
|
|
695
|
+
return { pass, subcode: pass ? null : 'PROMOTION_INSUFFICIENT_SAMPLE' };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export interface RolloutCoverage {
|
|
699
|
+
capabilityDecision: number;
|
|
700
|
+
requiredCaps: number;
|
|
701
|
+
wouldBlock: number;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export const WARN_TO_ENFORCE_COVERAGE_FLOORS: RolloutCoverage = {
|
|
705
|
+
capabilityDecision: 99.0,
|
|
706
|
+
requiredCaps: 99.0,
|
|
707
|
+
wouldBlock: 99.5,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
export function hasCoverageForEnforce(coverage: RolloutCoverage): { pass: boolean; subcode: string | null } {
|
|
711
|
+
const pass =
|
|
712
|
+
coverage.capabilityDecision >= WARN_TO_ENFORCE_COVERAGE_FLOORS.capabilityDecision
|
|
713
|
+
&& coverage.requiredCaps >= WARN_TO_ENFORCE_COVERAGE_FLOORS.requiredCaps
|
|
714
|
+
&& coverage.wouldBlock >= WARN_TO_ENFORCE_COVERAGE_FLOORS.wouldBlock;
|
|
715
|
+
return { pass, subcode: pass ? null : 'PROMOTION_COVERAGE_INCOMPLETE' };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export type BreakGlassScopeDimension = 'environment' | 'region' | 'profile' | 'agentCohort';
|
|
719
|
+
|
|
720
|
+
export function validateBreakGlassRequest(params: {
|
|
721
|
+
scope: Partial<Record<BreakGlassScopeDimension, string>>;
|
|
722
|
+
affectedAgents: number;
|
|
723
|
+
activeFleetAgents: number;
|
|
724
|
+
approvals: number;
|
|
725
|
+
ttlMs: number;
|
|
726
|
+
}): { broadScope: boolean } {
|
|
727
|
+
if (params.ttlMs > 4 * 60 * 60_000) throw new Error('BREAK_GLASS_TTL_EXCEEDED');
|
|
728
|
+
if (params.activeFleetAgents <= 0) throw new Error('BREAK_GLASS_INVALID_FLEET');
|
|
729
|
+
|
|
730
|
+
const scopeKeys = Object.keys(params.scope) as BreakGlassScopeDimension[];
|
|
731
|
+
if (scopeKeys.length === 0) throw new Error('BREAK_GLASS_SCOPE_REQUIRED');
|
|
732
|
+
|
|
733
|
+
const affectedPct = params.affectedAgents / params.activeFleetAgents;
|
|
734
|
+
const broadScope = affectedPct > 0.2;
|
|
735
|
+
const requiredApprovals = broadScope ? 2 : 1;
|
|
736
|
+
if (params.approvals < requiredApprovals) throw new Error('BREAK_GLASS_APPROVAL_REQUIRED');
|
|
737
|
+
|
|
738
|
+
return { broadScope };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export const CONFORMANCE_MAX_AGE_MS = 7 * 24 * 60 * 60_000;
|
|
742
|
+
|
|
743
|
+
export function validateConformanceArtifact(params: {
|
|
744
|
+
generatedAtMs: number;
|
|
745
|
+
nowMs: number;
|
|
746
|
+
currentServerBuild: string;
|
|
747
|
+
artifactServerBuild: string;
|
|
748
|
+
currentRegistryVersion: string;
|
|
749
|
+
artifactRegistryVersion: string;
|
|
750
|
+
}): { pass: boolean; subcode: string | null } {
|
|
751
|
+
const ageMs = params.nowMs - params.generatedAtMs;
|
|
752
|
+
if (ageMs > CONFORMANCE_MAX_AGE_MS) return { pass: false, subcode: 'PROMOTION_CONFORMANCE_STALE' };
|
|
753
|
+
if (params.currentServerBuild !== params.artifactServerBuild) return { pass: false, subcode: 'PROMOTION_CONFORMANCE_STALE' };
|
|
754
|
+
if (params.currentRegistryVersion !== params.artifactRegistryVersion) {
|
|
755
|
+
return { pass: false, subcode: 'PROMOTION_CONFORMANCE_STALE' };
|
|
756
|
+
}
|
|
757
|
+
return { pass: true, subcode: null };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function resolveUnknownMajorSchemaOutcome(params: {
|
|
761
|
+
policyMode: RolloutPolicyMode;
|
|
762
|
+
unresolvedSamples: number;
|
|
763
|
+
currentlyEnforced: boolean;
|
|
764
|
+
}): {
|
|
765
|
+
shouldPage: boolean;
|
|
766
|
+
allowOperation: boolean;
|
|
767
|
+
promotionBlocked: boolean;
|
|
768
|
+
enforceHold: boolean;
|
|
769
|
+
subcode: string;
|
|
770
|
+
} {
|
|
771
|
+
if (params.unresolvedSamples <= 0) {
|
|
772
|
+
return {
|
|
773
|
+
shouldPage: false,
|
|
774
|
+
allowOperation: true,
|
|
775
|
+
promotionBlocked: false,
|
|
776
|
+
enforceHold: false,
|
|
777
|
+
subcode: 'SCHEMA_MAJOR_OK',
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (params.policyMode === 'enforce') {
|
|
782
|
+
return {
|
|
783
|
+
shouldPage: true,
|
|
784
|
+
allowOperation: false,
|
|
785
|
+
promotionBlocked: true,
|
|
786
|
+
enforceHold: params.currentlyEnforced,
|
|
787
|
+
subcode: 'SCHEMA_MAJOR_UNKNOWN_ENFORCED',
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
shouldPage: true,
|
|
793
|
+
allowOperation: true,
|
|
794
|
+
promotionBlocked: true,
|
|
795
|
+
enforceHold: false,
|
|
796
|
+
subcode: 'SCHEMA_MAJOR_UNKNOWN',
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Loop 6 frozen contracts: drift reconciliation, host-transfer linearization, exception composition,
|
|
801
|
+
// forensics signer trust-chain ingestion, anti-flap budget governance, and waiver artifact enforcement.
|
|
802
|
+
export type DriftSourceFamily =
|
|
803
|
+
| 'transport_tls'
|
|
804
|
+
| 'socket_identity'
|
|
805
|
+
| 'identity_fingerprint'
|
|
806
|
+
| 'policy_integrity'
|
|
807
|
+
| 'runtime_capability';
|
|
808
|
+
|
|
809
|
+
export const DRIFT_FAMILY_ORDER: DriftSourceFamily[] = [
|
|
810
|
+
'transport_tls',
|
|
811
|
+
'socket_identity',
|
|
812
|
+
'identity_fingerprint',
|
|
813
|
+
'policy_integrity',
|
|
814
|
+
'runtime_capability',
|
|
815
|
+
];
|
|
816
|
+
|
|
817
|
+
export function canonicalizeDriftFamilies(input: DriftSourceFamily[]): DriftSourceFamily[] {
|
|
818
|
+
return [...new Set(input)].sort((a, b) => DRIFT_FAMILY_ORDER.indexOf(a) - DRIFT_FAMILY_ORDER.indexOf(b));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export interface DriftObservation {
|
|
822
|
+
family: DriftSourceFamily;
|
|
823
|
+
detectedAtMs: number;
|
|
824
|
+
seq: number;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function reconcileDriftObservations(observations: DriftObservation[]): {
|
|
828
|
+
order: DriftSourceFamily[];
|
|
829
|
+
driftReconciled: boolean;
|
|
830
|
+
} {
|
|
831
|
+
const sorted = [...observations].sort((a, b) => {
|
|
832
|
+
if (a.detectedAtMs !== b.detectedAtMs) return a.detectedAtMs - b.detectedAtMs;
|
|
833
|
+
if (a.seq !== b.seq) return a.seq - b.seq;
|
|
834
|
+
return DRIFT_FAMILY_ORDER.indexOf(a.family) - DRIFT_FAMILY_ORDER.indexOf(b.family);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
order: canonicalizeDriftFamilies(sorted.map((item) => item.family)),
|
|
839
|
+
driftReconciled: sorted.length > 0,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function resolveTransferCommit(params: {
|
|
844
|
+
phase: 'prepare' | 'commit' | 'finalize';
|
|
845
|
+
transferId: string;
|
|
846
|
+
expectedCommitIndex: number;
|
|
847
|
+
observedCommitIndex: number;
|
|
848
|
+
}): {
|
|
849
|
+
committed: boolean;
|
|
850
|
+
retryable: boolean;
|
|
851
|
+
finalStateKnown: boolean;
|
|
852
|
+
subcode: 'TRANSFER_COMMIT_APPLIED' | 'TRANSFER_COMMIT_ALREADY_APPLIED' | 'TRANSFER_COMMIT_CONFLICT';
|
|
853
|
+
} {
|
|
854
|
+
if (params.observedCommitIndex === params.expectedCommitIndex) {
|
|
855
|
+
return {
|
|
856
|
+
committed: true,
|
|
857
|
+
retryable: false,
|
|
858
|
+
finalStateKnown: true,
|
|
859
|
+
subcode: 'TRANSFER_COMMIT_APPLIED',
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (params.observedCommitIndex > params.expectedCommitIndex) {
|
|
864
|
+
return {
|
|
865
|
+
committed: true,
|
|
866
|
+
retryable: false,
|
|
867
|
+
finalStateKnown: true,
|
|
868
|
+
subcode: 'TRANSFER_COMMIT_ALREADY_APPLIED',
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
committed: false,
|
|
874
|
+
retryable: false,
|
|
875
|
+
finalStateKnown: false,
|
|
876
|
+
subcode: 'TRANSFER_COMMIT_CONFLICT',
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export const MAX_ACTIVE_EXCEPTIONS_PER_SCOPE = 3;
|
|
881
|
+
|
|
882
|
+
export type ExceptionSafetyInvariant = 'no_bypass_mfa' | 'no_plaintext_token' | 'no_unapproved_remote_write';
|
|
883
|
+
|
|
884
|
+
export function validateExceptionComposition(params: {
|
|
885
|
+
scopeKey: string;
|
|
886
|
+
activeExceptionCount: number;
|
|
887
|
+
overlays: string[];
|
|
888
|
+
requestedRelaxations: string[];
|
|
889
|
+
protectedInvariants: ExceptionSafetyInvariant[];
|
|
890
|
+
}): { accepted: boolean; effectiveRelaxations: string[]; subcode: string | null } {
|
|
891
|
+
if (params.activeExceptionCount >= MAX_ACTIVE_EXCEPTIONS_PER_SCOPE) {
|
|
892
|
+
return { accepted: false, effectiveRelaxations: [], subcode: 'EXCEPTION_ACTIVE_LIMIT' };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const overlap = params.requestedRelaxations.filter((r) => params.overlays.includes(r));
|
|
896
|
+
const wouldBypassInvariant = overlap.some((relaxation) => params.protectedInvariants.includes(relaxation as ExceptionSafetyInvariant));
|
|
897
|
+
if (wouldBypassInvariant) {
|
|
898
|
+
return { accepted: false, effectiveRelaxations: [], subcode: 'EXCEPTION_CONFLICTS_INVARIANT' };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
accepted: true,
|
|
903
|
+
effectiveRelaxations: overlap,
|
|
904
|
+
subcode: null,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export const SIGNER_ROTATION_OVERLAP_MS = 24 * 60 * 60_000;
|
|
909
|
+
|
|
910
|
+
export function validateForensicsSigner(params: {
|
|
911
|
+
signerId: string;
|
|
912
|
+
trustedSignerIds: string[];
|
|
913
|
+
overlapSignerIds: string[];
|
|
914
|
+
revokedSignerIds: string[];
|
|
915
|
+
nowMs: number;
|
|
916
|
+
signatureIssuedAtMs: number;
|
|
917
|
+
signerExpiresAtMs: number;
|
|
918
|
+
}): { accepted: boolean; subcode: string | null } {
|
|
919
|
+
if (params.revokedSignerIds.includes(params.signerId)) return { accepted: false, subcode: 'FORENSICS_SIGNER_REVOKED' };
|
|
920
|
+
if (params.nowMs > params.signerExpiresAtMs) return { accepted: false, subcode: 'FORENSICS_SIGNER_EXPIRED' };
|
|
921
|
+
|
|
922
|
+
const withinRotationWindow = params.nowMs - params.signatureIssuedAtMs <= SIGNER_ROTATION_OVERLAP_MS;
|
|
923
|
+
const active = params.trustedSignerIds.includes(params.signerId);
|
|
924
|
+
const overlap = params.overlapSignerIds.includes(params.signerId) && withinRotationWindow;
|
|
925
|
+
|
|
926
|
+
if (!active && !overlap) return { accepted: false, subcode: 'FORENSICS_SIGNER_UNTRUSTED' };
|
|
927
|
+
return { accepted: true, subcode: null };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export const ERROR_BUDGET_ENTER_PCT = 5;
|
|
931
|
+
export const ERROR_BUDGET_EXIT_PCT = 2;
|
|
932
|
+
export const ERROR_BUDGET_HOLD_DWELL_MS = 15 * 60_000;
|
|
933
|
+
|
|
934
|
+
export function resolveErrorBudgetGovernance(params: {
|
|
935
|
+
currentMode: 'enforce' | 'degraded';
|
|
936
|
+
budgetBurnPercent: number;
|
|
937
|
+
modeSinceMs: number;
|
|
938
|
+
nowMs: number;
|
|
939
|
+
severeIncidentOpen: boolean;
|
|
940
|
+
}): { nextMode: 'enforce' | 'degraded'; subcode: string } {
|
|
941
|
+
const dwellSatisfied = params.nowMs - params.modeSinceMs >= ERROR_BUDGET_HOLD_DWELL_MS;
|
|
942
|
+
if (params.currentMode === 'enforce') {
|
|
943
|
+
if (params.budgetBurnPercent >= ERROR_BUDGET_ENTER_PCT) return { nextMode: 'degraded', subcode: 'BUDGET_ENTER_DEGRADED' };
|
|
944
|
+
return { nextMode: 'enforce', subcode: 'BUDGET_STABLE' };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (params.severeIncidentOpen) return { nextMode: 'degraded', subcode: 'BUDGET_HOLD_SEVERE_INCIDENT' };
|
|
948
|
+
if (params.budgetBurnPercent <= ERROR_BUDGET_EXIT_PCT && dwellSatisfied) {
|
|
949
|
+
return { nextMode: 'enforce', subcode: 'BUDGET_EXIT_DEGRADED' };
|
|
950
|
+
}
|
|
951
|
+
return { nextMode: 'degraded', subcode: 'BUDGET_HOLD_DWELL' };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export const WAIVER_SCHEMA_VERSION = 'v1';
|
|
955
|
+
|
|
956
|
+
export function validateWaiverArtifact(params: {
|
|
957
|
+
schemaVersion: string;
|
|
958
|
+
scope: string;
|
|
959
|
+
approvalCount: number;
|
|
960
|
+
minApprovals: number;
|
|
961
|
+
expiresAtMs: number;
|
|
962
|
+
nowMs: number;
|
|
963
|
+
signature: string | null;
|
|
964
|
+
}): { accepted: boolean; subcode: string | null } {
|
|
965
|
+
if (params.schemaVersion !== WAIVER_SCHEMA_VERSION) return { accepted: false, subcode: 'WAIVER_SCHEMA_UNSUPPORTED' };
|
|
966
|
+
if (!params.signature) return { accepted: false, subcode: 'WAIVER_SIGNATURE_REQUIRED' };
|
|
967
|
+
if (!params.scope) return { accepted: false, subcode: 'WAIVER_SCOPE_REQUIRED' };
|
|
968
|
+
if (params.approvalCount < params.minApprovals) return { accepted: false, subcode: 'WAIVER_APPROVALS_INSUFFICIENT' };
|
|
969
|
+
if (params.nowMs > params.expiresAtMs) return { accepted: false, subcode: 'WAIVER_EXPIRED' };
|
|
970
|
+
return { accepted: true, subcode: null };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Loop 7 frozen contracts: delegated high-risk execution, partition journal trust-chain,
|
|
974
|
+
// approval SoD freshness, crypto migration matrix, compliance closure, and causal precedence.
|
|
975
|
+
export const DELEGATION_MAX_SKEW_MS = 30_000;
|
|
976
|
+
|
|
977
|
+
export type DelegationRuntimeMode = 'local' | 'remote' | 'headless';
|
|
978
|
+
|
|
979
|
+
export interface DelegationBindingTuple {
|
|
980
|
+
delegationId: string;
|
|
981
|
+
principalCanonicalId: string;
|
|
982
|
+
runtimeMode: DelegationRuntimeMode;
|
|
983
|
+
operationClass: string;
|
|
984
|
+
subjectDigest: string;
|
|
985
|
+
constraintsDigest: string;
|
|
986
|
+
policyHash: string;
|
|
987
|
+
approvalId: string;
|
|
988
|
+
nonce: string;
|
|
989
|
+
expiresAt: string;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function normalizePrincipalCanonicalId(input: string): string {
|
|
993
|
+
return input.normalize('NFC').trim().toLowerCase();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function canonicalizeDelegationBinding(input: DelegationBindingTuple): DelegationBindingTuple {
|
|
997
|
+
return {
|
|
998
|
+
...input,
|
|
999
|
+
principalCanonicalId: normalizePrincipalCanonicalId(input.principalCanonicalId),
|
|
1000
|
+
runtimeMode: input.runtimeMode,
|
|
1001
|
+
operationClass: input.operationClass.trim(),
|
|
1002
|
+
subjectDigest: input.subjectDigest.trim(),
|
|
1003
|
+
constraintsDigest: input.constraintsDigest.trim(),
|
|
1004
|
+
policyHash: input.policyHash.trim(),
|
|
1005
|
+
approvalId: input.approvalId.trim(),
|
|
1006
|
+
nonce: input.nonce.trim(),
|
|
1007
|
+
expiresAt: new Date(input.expiresAt).toISOString(),
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function isDelegationExpired(expiresAtIso: string, nowMs: number, skewMs = DELEGATION_MAX_SKEW_MS): boolean {
|
|
1012
|
+
const exp = new Date(expiresAtIso).getTime();
|
|
1013
|
+
return !Number.isFinite(exp) || nowMs > exp + skewMs;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function resolveDelegationRevocationPrecedence(params: {
|
|
1017
|
+
revocationEventIndex: number | null;
|
|
1018
|
+
executionCommitIndex: number;
|
|
1019
|
+
}): { allowed: boolean; subcode: string | null } {
|
|
1020
|
+
if (params.revocationEventIndex === null) return { allowed: true, subcode: null };
|
|
1021
|
+
if (params.revocationEventIndex <= params.executionCommitIndex) {
|
|
1022
|
+
return { allowed: false, subcode: 'AUTH_DELEGATION_REVOKED' };
|
|
1023
|
+
}
|
|
1024
|
+
return { allowed: true, subcode: null };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export interface PartitionJournalEntry {
|
|
1028
|
+
epoch: number;
|
|
1029
|
+
seq: number;
|
|
1030
|
+
entryHash: string;
|
|
1031
|
+
prevEntryHash: string;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export function validatePartitionJournalChain(entries: PartitionJournalEntry[]): {
|
|
1035
|
+
accepted: boolean;
|
|
1036
|
+
subcode: 'JOURNAL_OK' | 'QUARANTINED_GAP' | 'QUARANTINED_FORK';
|
|
1037
|
+
} {
|
|
1038
|
+
const byEpoch = new Map<number, PartitionJournalEntry[]>();
|
|
1039
|
+
for (const entry of entries) {
|
|
1040
|
+
const list = byEpoch.get(entry.epoch) ?? [];
|
|
1041
|
+
list.push(entry);
|
|
1042
|
+
byEpoch.set(entry.epoch, list);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
for (const list of byEpoch.values()) {
|
|
1046
|
+
const seen = new Map<number, string>();
|
|
1047
|
+
for (const item of list) {
|
|
1048
|
+
const existing = seen.get(item.seq);
|
|
1049
|
+
if (existing && existing !== item.entryHash) return { accepted: false, subcode: 'QUARANTINED_FORK' };
|
|
1050
|
+
if (!existing) seen.set(item.seq, item.entryHash);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const ordered = [...list].sort((a, b) => a.seq - b.seq);
|
|
1054
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1055
|
+
if (ordered[i].seq !== i + 1) return { accepted: false, subcode: 'QUARANTINED_GAP' };
|
|
1056
|
+
if (i > 0 && ordered[i].prevEntryHash !== ordered[i - 1].entryHash) {
|
|
1057
|
+
return { accepted: false, subcode: 'QUARANTINED_GAP' };
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return { accepted: true, subcode: 'JOURNAL_OK' };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export function evaluateApprovalSet(params: {
|
|
1066
|
+
threshold: number;
|
|
1067
|
+
minDistinctTeams: number;
|
|
1068
|
+
minDistinctDomains: number;
|
|
1069
|
+
approvalMaxAgeMs: number;
|
|
1070
|
+
nowMs: number;
|
|
1071
|
+
approvals: Array<{ actorCanonicalKey: string; team: string; domain: string; approvedAtMs: number; revoked?: boolean }>;
|
|
1072
|
+
}): { accepted: boolean; subcode: string | null; uniqueActors: number } {
|
|
1073
|
+
const dedup = new Map<string, { team: string; domain: string; approvedAtMs: number; revoked?: boolean }>();
|
|
1074
|
+
for (const approval of params.approvals) {
|
|
1075
|
+
if (!dedup.has(approval.actorCanonicalKey)) dedup.set(approval.actorCanonicalKey, approval);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const unique = [...dedup.values()];
|
|
1079
|
+
const hasInvalid = unique.some((a) => a.revoked || params.nowMs - a.approvedAtMs > params.approvalMaxAgeMs);
|
|
1080
|
+
if (hasInvalid) return { accepted: false, subcode: 'AUTH_APPROVAL_SET_INVALIDATED', uniqueActors: unique.length };
|
|
1081
|
+
|
|
1082
|
+
const uniqueTeams = new Set(unique.map((a) => a.team));
|
|
1083
|
+
const uniqueDomains = new Set(unique.map((a) => a.domain));
|
|
1084
|
+
const sodPass = uniqueTeams.size >= params.minDistinctTeams && uniqueDomains.size >= params.minDistinctDomains;
|
|
1085
|
+
if (unique.length < params.threshold || !sodPass) {
|
|
1086
|
+
return { accepted: false, subcode: 'AUTH_APPROVAL_SOD_VIOLATION', uniqueActors: unique.length };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return { accepted: true, subcode: null, uniqueActors: unique.length };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export type CryptoPhase = 'PREPARE' | 'DUAL_VERIFY' | 'DUAL_SIGN_VERIFY' | 'NEW_PRIMARY' | 'RETIRE_OLD';
|
|
1093
|
+
|
|
1094
|
+
export const CRYPTO_PHASE_MATRIX: Record<CryptoPhase, { S_old: 0 | 1; S_new: 0 | 1; V_old: 0 | 1; V_new: 0 | 1 }> = {
|
|
1095
|
+
PREPARE: { S_old: 1, S_new: 0, V_old: 1, V_new: 0 },
|
|
1096
|
+
DUAL_VERIFY: { S_old: 1, S_new: 0, V_old: 1, V_new: 1 },
|
|
1097
|
+
DUAL_SIGN_VERIFY: { S_old: 1, S_new: 1, V_old: 1, V_new: 1 },
|
|
1098
|
+
NEW_PRIMARY: { S_old: 0, S_new: 1, V_old: 1, V_new: 1 },
|
|
1099
|
+
RETIRE_OLD: { S_old: 0, S_new: 1, V_old: 0, V_new: 1 },
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
const CRYPTO_PHASE_ORDER: CryptoPhase[] = ['PREPARE', 'DUAL_VERIFY', 'DUAL_SIGN_VERIFY', 'NEW_PRIMARY', 'RETIRE_OLD'];
|
|
1103
|
+
|
|
1104
|
+
export function validateCryptoPhaseState(phase: CryptoPhase, state: { S_old: number; S_new: number; V_old: number; V_new: number }): {
|
|
1105
|
+
accepted: boolean;
|
|
1106
|
+
subcode: string | null;
|
|
1107
|
+
} {
|
|
1108
|
+
const expected = CRYPTO_PHASE_MATRIX[phase];
|
|
1109
|
+
const accepted = expected.S_old === state.S_old
|
|
1110
|
+
&& expected.S_new === state.S_new
|
|
1111
|
+
&& expected.V_old === state.V_old
|
|
1112
|
+
&& expected.V_new === state.V_new;
|
|
1113
|
+
return { accepted, subcode: accepted ? null : 'AUTH_CRYPTO_PHASE_INVALID' };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function validateCryptoRollback(params: {
|
|
1117
|
+
from: CryptoPhase;
|
|
1118
|
+
to: CryptoPhase;
|
|
1119
|
+
targetPreconditionsMet: boolean;
|
|
1120
|
+
}): { allowed: boolean; subcode: string | null } {
|
|
1121
|
+
const fromIdx = CRYPTO_PHASE_ORDER.indexOf(params.from);
|
|
1122
|
+
const toIdx = CRYPTO_PHASE_ORDER.indexOf(params.to);
|
|
1123
|
+
const oneStepBack = toIdx === fromIdx - 1;
|
|
1124
|
+
if (!oneStepBack || !params.targetPreconditionsMet) {
|
|
1125
|
+
return { allowed: false, subcode: 'AUTH_CRYPTO_ROLLBACK_NOT_PERMITTED' };
|
|
1126
|
+
}
|
|
1127
|
+
return { allowed: true, subcode: null };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export type ComplianceMode = 'observe' | 'warn' | 'enforce';
|
|
1131
|
+
export type ComplianceClosureStatus = 'CLOSED_COMPLETE' | 'CLOSED_INCOMPLETE' | 'PENDING_EVIDENCE';
|
|
1132
|
+
|
|
1133
|
+
export function evaluateComplianceClosure(params: {
|
|
1134
|
+
mode: ComplianceMode;
|
|
1135
|
+
manifestSchemaVersion: string;
|
|
1136
|
+
closureStatus: ComplianceClosureStatus;
|
|
1137
|
+
waiverLinked: boolean;
|
|
1138
|
+
}): { accepted: boolean; subcode: string | null; effectiveStatus: ComplianceClosureStatus } {
|
|
1139
|
+
const major = params.manifestSchemaVersion.match(/^(\d+)\./)?.[1] ?? params.manifestSchemaVersion.match(/^v(\d+)/)?.[1] ?? '0';
|
|
1140
|
+
const knownMajor = major === '1';
|
|
1141
|
+
|
|
1142
|
+
if (!knownMajor) {
|
|
1143
|
+
if (params.mode === 'enforce') return { accepted: false, subcode: 'AUTH_COMPLIANCE_SCHEMA_UNSUPPORTED', effectiveStatus: 'PENDING_EVIDENCE' };
|
|
1144
|
+
return { accepted: false, subcode: 'AUTH_COMPLIANCE_SCHEMA_UNSUPPORTED', effectiveStatus: 'PENDING_EVIDENCE' };
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (params.closureStatus === 'CLOSED_INCOMPLETE' && !params.waiverLinked) {
|
|
1148
|
+
return { accepted: false, subcode: 'AUTH_COMPLIANCE_WAIVER_REQUIRED', effectiveStatus: 'PENDING_EVIDENCE' };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return { accepted: true, subcode: null, effectiveStatus: params.closureStatus };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export function resolveCausalConflictPrecedence(params: {
|
|
1155
|
+
revocation: boolean;
|
|
1156
|
+
policyHashMismatch: boolean;
|
|
1157
|
+
approvalInvalid: boolean;
|
|
1158
|
+
delegationInvalid: boolean;
|
|
1159
|
+
reconciliationSuperseded: boolean;
|
|
1160
|
+
authoritativeEventRef: string;
|
|
1161
|
+
}): {
|
|
1162
|
+
allowed: boolean;
|
|
1163
|
+
supersessionReason: string | null;
|
|
1164
|
+
precedenceRuleId: number | null;
|
|
1165
|
+
authoritativeEventRef: string | null;
|
|
1166
|
+
requiredOperatorAction: string | null;
|
|
1167
|
+
} {
|
|
1168
|
+
const ordered: Array<{ when: boolean; reason: string; ruleId: number; action: string }> = [
|
|
1169
|
+
{ when: params.revocation, reason: 'explicit_revocation', ruleId: 1, action: 're-authorize_with_new_approval' },
|
|
1170
|
+
{ when: params.policyHashMismatch, reason: 'policy_hash_mismatch', ruleId: 2, action: 're-run_with_current_policy' },
|
|
1171
|
+
{ when: params.approvalInvalid, reason: 'approval_invalidated', ruleId: 3, action: 'collect_fresh_sod_approvals' },
|
|
1172
|
+
{ when: params.delegationInvalid, reason: 'delegation_invalid', ruleId: 4, action: 'mint_new_delegation' },
|
|
1173
|
+
{ when: params.reconciliationSuperseded, reason: 'reconciliation_superseded', ruleId: 5, action: 'reconcile_then_retry' },
|
|
1174
|
+
];
|
|
1175
|
+
|
|
1176
|
+
const match = ordered.find((item) => item.when);
|
|
1177
|
+
if (!match) {
|
|
1178
|
+
return {
|
|
1179
|
+
allowed: true,
|
|
1180
|
+
supersessionReason: null,
|
|
1181
|
+
precedenceRuleId: null,
|
|
1182
|
+
authoritativeEventRef: null,
|
|
1183
|
+
requiredOperatorAction: null,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return {
|
|
1188
|
+
allowed: false,
|
|
1189
|
+
supersessionReason: match.reason,
|
|
1190
|
+
precedenceRuleId: match.ruleId,
|
|
1191
|
+
authoritativeEventRef: params.authoritativeEventRef,
|
|
1192
|
+
requiredOperatorAction: match.action,
|
|
1193
|
+
};
|
|
1194
|
+
}
|