feat(phase-5): WS5 — Developer Experience

Implements scaffold ZIP generator, Stoplight Elements API explorer, and CLI scaffold command:

Scaffold API:
- 25 template files for TypeScript/Python/Go/Java/Rust in src/templates/scaffold/
- ScaffoldService: in-memory ZIP via archiver, variable injection (AGENT_ID/NAME/CLIENT_ID/API_URL)
- ScaffoldController: tenant ownership check (403), language validation (400), ZIP stream response
- Route GET /sdk/scaffold/:agentId with rate limiter (10 req/min per tenant)
- Prometheus: scaffold_generated_total + scaffold_generation_duration_ms histogram

Portal:
- Replaced swagger-ui-react with @stoplight/elements API component
- Dynamic import (ssr: false) for browser-only DOM dependency
- Type declarations for @stoplight/elements and CSS module

CLI:
- sentryagent scaffold --agent-id <id> [--language typescript] [--out .]
- Raw fetch for binary ZIP stream → unzipper.Extract() → prints next steps
- Human-readable 400/403/404 error messages

Tests: 19 tests (unit + integration), ScaffoldService 80%+ branch coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-03 02:50:32 +00:00
parent 16497706d3
commit 662879f0ee
42 changed files with 6176 additions and 1741 deletions

150
cli/package-lock.json generated
View File

@@ -9,8 +9,10 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/unzipper": "^0.10.11",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.1.0" "commander": "^12.1.0",
"unzipper": "^0.12.3"
}, },
"bin": { "bin": {
"sentryagent": "dist/index.js" "sentryagent": "dist/index.js"
@@ -97,12 +99,20 @@
"version": "20.19.37", "version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-D25im2zjyMCcgL9ag6N46+wbtJBnXIr7SI4zHf9eJD2Dw2tEB5e+p5MYkrxKIVRscs5QV0EhtU9rgXSPx90oJg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -136,6 +146,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"license": "MIT"
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "5.6.2", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
@@ -157,6 +173,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/create-require": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -174,6 +196,59 @@
"node": ">=0.3.1" "node": ">=0.3.1"
} }
}, },
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"license": "BSD-3-Clause",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/fs-extra": {
"version": "11.3.4",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/make-error": { "node_modules/make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -181,6 +256,48 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"license": "MIT"
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/ts-node": { "node_modules/ts-node": {
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -243,7 +360,34 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unzipper": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz",
"integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==",
"license": "MIT",
"dependencies": {
"bluebird": "~3.7.2",
"duplexer2": "~0.1.4",
"fs-extra": "^11.2.0",
"graceful-fs": "^4.2.2",
"node-int64": "^0.4.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/v8-compile-cache-lib": { "node_modules/v8-compile-cache-lib": {

View File

@@ -12,13 +12,15 @@
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"dependencies": { "dependencies": {
"@types/unzipper": "^0.10.11",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.1.0" "commander": "^12.1.0",
"unzipper": "^0.12.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"typescript": "^5.4.5", "ts-node": "^10.9.2",
"ts-node": "^10.9.2" "typescript": "^5.4.5"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"

View File

@@ -0,0 +1,173 @@
import * as fs from 'fs';
import * as path from 'path';
import { Command } from 'commander';
import chalk from 'chalk';
import unzipper from 'unzipper';
import { requireConfig } from '../config';
const VALID_LANGUAGES = ['typescript', 'python', 'go', 'java', 'rust'] as const;
type ScaffoldLanguage = (typeof VALID_LANGUAGES)[number];
function isValidLanguage(lang: string): lang is ScaffoldLanguage {
return (VALID_LANGUAGES as readonly string[]).includes(lang);
}
export function registerScaffold(program: Command): void {
program
.command('scaffold')
.description('Download a starter project scaffold pre-wired with your agent credentials')
.requiredOption('--agent-id <id>', 'Agent ID to scaffold for')
.option(
'--language <lang>',
`SDK language (${VALID_LANGUAGES.join(', ')})`,
'typescript',
)
.option('--out <directory>', 'Output directory for the extracted scaffold', '.')
.action(async (opts: { agentId: string; language: string; out: string }) => {
const { agentId, language, out: outDir } = opts;
if (!isValidLanguage(language)) {
console.error(
chalk.red('Error:'),
`Unsupported language '${language}'. Choose: ${VALID_LANGUAGES.join(', ')}`,
);
process.exit(1);
}
const config = requireConfig();
// Resolve and create output directory
const resolvedOut = path.resolve(outDir);
if (!fs.existsSync(resolvedOut)) {
fs.mkdirSync(resolvedOut, { recursive: true });
}
console.log(
chalk.dim(`Downloading ${language} scaffold for agent ${agentId}...`),
);
try {
// We need a raw binary response — fetch the token via apiRequest pattern
// then make a raw fetch for the ZIP stream.
const token = await getToken(config);
const url = `${config.apiUrl}/sdk/scaffold/${encodeURIComponent(agentId)}?language=${encodeURIComponent(language)}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const text = await res.text();
handleHttpError(res.status, text);
process.exit(1);
}
if (res.body === null) {
console.error(chalk.red('Error:'), 'Empty response body from server.');
process.exit(1);
}
// Pipe the response body through unzipper into the output directory
await new Promise<void>((resolve, reject) => {
const nodeStream = streamFromWeb(res.body!);
nodeStream
.pipe(unzipper.Extract({ path: resolvedOut }))
.on('close', resolve)
.on('error', reject);
});
console.log(chalk.green('Scaffold extracted to:'), chalk.bold(resolvedOut));
console.log('');
console.log('Next steps:');
console.log(
` 1. ${chalk.cyan('cd')} ${resolvedOut}`,
);
if (language === 'typescript') {
console.log(` 2. ${chalk.cyan('npm install')}`);
console.log(` 3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
console.log(` 4. ${chalk.cyan('npm run dev')}`);
} else if (language === 'python') {
console.log(` 2. ${chalk.cyan('pip install -r requirements.txt')}`);
console.log(` 3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
console.log(` 4. ${chalk.cyan('python main.py')}`);
} else if (language === 'go') {
console.log(` 2. ${chalk.cyan('go mod download')}`);
console.log(` 3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
console.log(` 4. ${chalk.cyan('go run main.go')}`);
} else if (language === 'java') {
console.log(` 2. ${chalk.cyan('mvn install')}`);
console.log(` 3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
console.log(` 4. ${chalk.cyan('mvn exec:java')}`);
} else if (language === 'rust') {
console.log(` 2. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
console.log(` 3. ${chalk.cyan('cargo run')}`);
}
} catch (err) {
console.error(
chalk.red('Error:'),
err instanceof Error ? err.message : String(err),
);
process.exit(1);
}
});
}
/** Obtain a bearer token by making a dummy apiRequest that uses the token cache. */
async function getToken(config: import('../config').Config): Promise<string> {
// apiRequest internally calls fetchToken which caches tokens.
// We retrieve the token by triggering any valid request, but that's wasteful.
// Instead, duplicate the token fetch logic inline to avoid making an extra API call.
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
});
const res = await fetch(`${config.apiUrl}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Authentication failed (${res.status}): ${text}`);
}
const data = (await res.json()) as { access_token: string };
return data.access_token;
}
function handleHttpError(status: number, body: string): void {
if (status === 400) {
console.error(chalk.red('Error:'), `Invalid request: ${body}`);
} else if (status === 401) {
console.error(
chalk.red('Error:'),
'Authentication failed. Run `sentryagent configure` to update credentials.',
);
} else if (status === 403) {
console.error(
chalk.red('Error:'),
'Access denied. You do not own this agent.',
);
} else if (status === 404) {
console.error(
chalk.red('Error:'),
'Agent not found. Check the agent ID with `sentryagent list-agents`.',
);
} else {
console.error(chalk.red('Error:'), `Server error (${status}): ${body}`);
}
}
/**
* Converts a WHATWG ReadableStream (from fetch) to a Node.js Readable stream.
* Node 18+ supports ReadableStream natively via stream.Readable.fromWeb().
*/
function streamFromWeb(webStream: ReadableStream<Uint8Array>): NodeJS.ReadableStream {
// Node.js 18+ has stream.Readable.fromWeb
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Readable } = require('stream') as typeof import('stream');
return Readable.fromWeb(webStream as Parameters<typeof Readable.fromWeb>[0]) as NodeJS.ReadableStream;
}

View File

@@ -10,6 +10,7 @@ import { registerIssueToken } from './commands/issue-token';
import { registerRotateCredentials } from './commands/rotate-credentials'; import { registerRotateCredentials } from './commands/rotate-credentials';
import { registerTailAuditLog } from './commands/tail-audit-log'; import { registerTailAuditLog } from './commands/tail-audit-log';
import { registerCompletion } from './commands/completion'; import { registerCompletion } from './commands/completion';
import { registerScaffold } from './commands/scaffold';
const program = new Command(); const program = new Command();
@@ -26,6 +27,7 @@ registerIssueToken(program);
registerRotateCredentials(program); registerRotateCredentials(program);
registerTailAuditLog(program); registerTailAuditLog(program);
registerCompletion(program); registerCompletion(program);
registerScaffold(program);
// Parse args — commander will display help automatically on --help // Parse args — commander will display help automatically on --help
program.parse(process.argv); program.parse(process.argv);

View File

@@ -1,100 +1,100 @@
## 1. WS1: Rust SDK — Crate Setup ## 1. WS1: Rust SDK — Crate Setup
- [ ] 1.1 Create `sdk-rust/` directory and `Cargo.toml` — name: `sentryagent-idp`, version: `1.0.0`, edition: `2021`; add dependencies: `tokio` (features: full), `reqwest` (features: json, rustls-tls), `serde` (features: derive), `serde_json`, `uuid` (features: v4), `thiserror`, `async-trait`; add dev-dependencies: `tokio-test`, `mockito` - [x] 1.1 Create `sdk-rust/` directory and `Cargo.toml` — name: `sentryagent-idp`, version: `1.0.0`, edition: `2021`; add dependencies: `tokio` (features: full), `reqwest` (features: json, rustls-tls), `serde` (features: derive), `serde_json`, `uuid` (features: v4), `thiserror`, `async-trait`; add dev-dependencies: `tokio-test`, `mockito`
- [ ] 1.2 Create `sdk-rust/src/lib.rs` — crate root with `#![deny(warnings)]`; re-export `AgentIdPClient`, `TokenManager`, `AgentIdPError`, and all model types from submodules; add crate-level `//!` doc comment describing the SDK - [x] 1.2 Create `sdk-rust/src/lib.rs` — crate root with `#![deny(warnings)]`; re-export `AgentIdPClient`, `TokenManager`, `AgentIdPError`, and all model types from submodules; add crate-level `//!` doc comment describing the SDK
- [ ] 1.3 Create `sdk-rust/src/error.rs` — define `AgentIdPError` enum with variants: `HttpError(reqwest::Error)`, `ApiError { status: u16, message: String, code: Option<String> }`, `AuthError(String)`, `NotFound(String)`, `RateLimited { retry_after_secs: u64 }`, `ConfigError(String)`, `SerdeError(serde_json::Error)`, `DelegationError(String)`; derive `thiserror::Error` and `Debug`; implement `std::error::Error` - [x] 1.3 Create `sdk-rust/src/error.rs` — define `AgentIdPError` enum with variants: `HttpError(reqwest::Error)`, `ApiError { status: u16, message: String, code: Option<String> }`, `AuthError(String)`, `NotFound(String)`, `RateLimited { retry_after_secs: u64 }`, `ConfigError(String)`, `SerdeError(serde_json::Error)`, `DelegationError(String)`; derive `thiserror::Error` and `Debug`; implement `std::error::Error`
- [ ] 1.4 Create `sdk-rust/src/models.rs` — define all request structs (`RegisterAgentRequest`, `UpdateAgentRequest`, `AuditLogFilters`, `MarketplaceFilters`, `DelegateRequest`) and all response structs (`Agent`, `AgentList`, `TokenResponse`, `Credentials`, `AuditLogEntry`, `AuditLogList`, `MarketplaceAgent`, `MarketplaceAgentList`, `DelegationToken`, `DelegationVerification`); all structs derive `serde::Serialize`, `serde::Deserialize`, `Debug`, `Clone` - [x] 1.4 Create `sdk-rust/src/models.rs` — define all request structs (`RegisterAgentRequest`, `UpdateAgentRequest`, `AuditLogFilters`, `MarketplaceFilters`, `DelegateRequest`) and all response structs (`Agent`, `AgentList`, `TokenResponse`, `Credentials`, `AuditLogEntry`, `AuditLogList`, `MarketplaceAgent`, `MarketplaceAgentList`, `DelegationToken`, `DelegationVerification`); all structs derive `serde::Serialize`, `serde::Deserialize`, `Debug`, `Clone`
## 2. WS1: Rust SDK — Token Manager ## 2. WS1: Rust SDK — Token Manager
- [ ] 2.1 Create `sdk-rust/src/token_manager.rs` — define `TokenCache` struct with `access_token: Option<String>` and `expires_at: Option<std::time::Instant>`; define `TokenManager` struct with fields `api_url`, `client_id`, `client_secret`, `cache: Arc<Mutex<TokenCache>>` - [x] 2.1 Create `sdk-rust/src/token_manager.rs` — define `TokenCache` struct with `access_token: Option<String>` and `expires_at: Option<std::time::Instant>`; define `TokenManager` struct with fields `api_url`, `client_id`, `client_secret`, `cache: Arc<Mutex<TokenCache>>`
- [ ] 2.2 Implement `TokenManager::new(api_url: &str, client_id: &str, client_secret: &str) -> Self` — initializes with empty cache - [x] 2.2 Implement `TokenManager::new(api_url: &str, client_id: &str, client_secret: &str) -> Self` — initializes with empty cache
- [ ] 2.3 Implement `TokenManager::get_token(&self) -> Result<String, AgentIdPError>` — acquires lock, checks `expires_at` against `Instant::now() + 60s`, returns cached token if valid, else calls `POST /oauth2/token` via `reqwest`, updates cache, releases lock - [x] 2.3 Implement `TokenManager::get_token(&self) -> Result<String, AgentIdPError>` — acquires lock, checks `expires_at` against `Instant::now() + 60s`, returns cached token if valid, else calls `POST /oauth2/token` via `reqwest`, updates cache, releases lock
- [ ] 2.4 Write unit test `token_manager_returns_cached_token` — mock `POST /oauth2/token` using `mockito`, call `get_token()` twice, verify mock is hit only once - [x] 2.4 Write unit test `token_manager_returns_cached_token` — mock `POST /oauth2/token` using `mockito`, call `get_token()` twice, verify mock is hit only once
- [ ] 2.5 Write unit test `token_manager_refreshes_expired_token` — set `expires_at` to past, verify `get_token()` triggers a new `POST /oauth2/token` call - [x] 2.5 Write unit test `token_manager_refreshes_expired_token` — set `expires_at` to past, verify `get_token()` triggers a new `POST /oauth2/token` call
- [ ] 2.6 Write concurrent safety test `token_manager_concurrent_calls_no_race` — spawn 50 `tokio::spawn` tasks all calling `get_token()` simultaneously, verify mock is hit at most once (no thundering herd), verify all 50 tasks receive valid tokens - [x] 2.6 Write concurrent safety test `token_manager_concurrent_calls_no_race` — spawn 50 `tokio::spawn` tasks all calling `get_token()` simultaneously, verify mock is hit at most once (no thundering herd), verify all 50 tasks receive valid tokens
## 3. WS1: Rust SDK — Client Methods ## 3. WS1: Rust SDK — Client Methods
- [ ] 3.1 Create `sdk-rust/src/client.rs` — define `AgentIdPClient` struct with fields `base_url`, `client_id`, `client_secret`, `http: reqwest::Client`, `token_manager: Arc<Mutex<TokenManager>>`; implement `new(base_url, client_id, client_secret) -> Self` and `from_env() -> Result<Self, AgentIdPError>` (reads `AGENTIDP_API_URL`, `AGENTIDP_CLIENT_ID`, `AGENTIDP_CLIENT_SECRET`) - [x] 3.1 Create `sdk-rust/src/client.rs` — define `AgentIdPClient` struct with fields `base_url`, `client_id`, `client_secret`, `http: reqwest::Client`, `token_manager: Arc<Mutex<TokenManager>>`; implement `new(base_url, client_id, client_secret) -> Self` and `from_env() -> Result<Self, AgentIdPError>` (reads `AGENTIDP_API_URL`, `AGENTIDP_CLIENT_ID`, `AGENTIDP_CLIENT_SECRET`)
- [ ] 3.2 Create `sdk-rust/src/agents.rs` — implement all agent methods on `AgentIdPClient`: `register_agent`, `get_agent`, `list_agents`, `update_agent`, `delete_agent` — each acquires a bearer token via `token_manager.get_token()`, makes the correct HTTP call, deserializes response, maps non-2xx responses to `AgentIdPError::ApiError` - [x] 3.2 Create `sdk-rust/src/agents.rs` — implement all agent methods on `AgentIdPClient`: `register_agent`, `get_agent`, `list_agents`, `update_agent`, `delete_agent` — each acquires a bearer token via `token_manager.get_token()`, makes the correct HTTP call, deserializes response, maps non-2xx responses to `AgentIdPError::ApiError`
- [ ] 3.3 Create `sdk-rust/src/oauth2.rs` — implement `issue_token(&self, agent_id: &str, scopes: &[&str]) -> Result<TokenResponse, AgentIdPError>` — sends `POST /oauth2/token` with `grant_type=client_credentials` - [x] 3.3 Create `sdk-rust/src/oauth2.rs` — implement `issue_token(&self, agent_id: &str, scopes: &[&str]) -> Result<TokenResponse, AgentIdPError>` — sends `POST /oauth2/token` with `grant_type=client_credentials`
- [ ] 3.4 Create `sdk-rust/src/credentials.rs` — implement `generate_credentials`, `rotate_credentials`, `revoke_credentials` — map 404 response to `AgentIdPError::NotFound`, map 401 to `AgentIdPError::AuthError` - [x] 3.4 Create `sdk-rust/src/credentials.rs` — implement `generate_credentials`, `rotate_credentials`, `revoke_credentials` — map 404 response to `AgentIdPError::NotFound`, map 401 to `AgentIdPError::AuthError`
- [ ] 3.5 Create `sdk-rust/src/audit.rs` — implement `list_audit_logs(filters: AuditLogFilters)` — serialize filters as query parameters; handle empty result set (return empty `Vec`, not error) - [x] 3.5 Create `sdk-rust/src/audit.rs` — implement `list_audit_logs(filters: AuditLogFilters)` — serialize filters as query parameters; handle empty result set (return empty `Vec`, not error)
- [ ] 3.6 Create `sdk-rust/src/marketplace.rs` — implement `list_public_agents(filters)` and `get_public_agent(agent_id)` — no auth header required for these endpoints - [x] 3.6 Create `sdk-rust/src/marketplace.rs` — implement `list_public_agents(filters)` and `get_public_agent(agent_id)` — no auth header required for these endpoints
- [ ] 3.7 Create `sdk-rust/src/delegation.rs` — implement `delegate(req: DelegateRequest)` and `verify_delegation(token: &str)` - [x] 3.7 Create `sdk-rust/src/delegation.rs` — implement `delegate(req: DelegateRequest)` and `verify_delegation(token: &str)`
- [ ] 3.8 Implement 429 handling across all client methods — parse `Retry-After` header, return `AgentIdPError::RateLimited { retry_after_secs }`; verify zero `unwrap()` calls in all `src/` files (run `grep -r 'unwrap()' sdk-rust/src/` — must return empty) - [x] 3.8 Implement 429 handling across all client methods — parse `Retry-After` header, return `AgentIdPError::RateLimited { retry_after_secs }`; verify zero `unwrap()` calls in all `src/` files (run `grep -r 'unwrap()' sdk-rust/src/` — must return empty)
## 4. WS1: Rust SDK — Tests, Examples, Documentation ## 4. WS1: Rust SDK — Tests, Examples, Documentation
- [ ] 4.1 Create `sdk-rust/examples/quickstart.rs` — working example: create `AgentIdPClient::from_env()`, call `register_agent`, call `issue_token`, print token; example must compile with `cargo build --example quickstart` - [x] 4.1 Create `sdk-rust/examples/quickstart.rs` — working example: create `AgentIdPClient::from_env()`, call `register_agent`, call `issue_token`, print token; example must compile with `cargo build --example quickstart`
- [ ] 4.2 Create `sdk-rust/tests/integration_test.rs` — integration tests requiring `AGENTIDP_API_URL`, `AGENTIDP_CLIENT_ID`, `AGENTIDP_CLIENT_SECRET` env vars; test: register agent, issue token, get agent, update agent, rotate credentials, delete agent; each test is `#[tokio::test]` with `#[ignore]` attribute (run explicitly with `cargo test -- --ignored`) - [x] 4.2 Create `sdk-rust/tests/integration_test.rs` — integration tests requiring `AGENTIDP_API_URL`, `AGENTIDP_CLIENT_ID`, `AGENTIDP_CLIENT_SECRET` env vars; test: register agent, issue token, get agent, update agent, rotate credentials, delete agent; each test is `#[tokio::test]` with `#[ignore]` attribute (run explicitly with `cargo test -- --ignored`)
- [ ] 4.3 Write `sdk-rust/README.md` — installation via `Cargo.toml`, environment variable configuration, quickstart code example, full method reference table with signatures, error handling guide, link to crates.io - [x] 4.3 Write `sdk-rust/README.md` — installation via `Cargo.toml`, environment variable configuration, quickstart code example, full method reference table with signatures, error handling guide, link to crates.io
- [ ] 4.4 Run `cargo doc --no-deps` — verify docs generate without errors or warnings; verify all public items have `///` doc comments - [x] 4.4 Run `cargo doc --no-deps` — verify docs generate without errors or warnings; verify all public items have `///` doc comments
- [ ] 4.5 Run `cargo clippy -- -D warnings` — zero warnings; run `cargo test` (unit tests only, no `--ignored`) — all pass - [x] 4.5 Run `cargo clippy -- -D warnings` — zero warnings; run `cargo test` (unit tests only, no `--ignored`) — all pass
## 5. WS2: A2A Authorization — Database & Types ## 5. WS2: A2A Authorization — Database & Types
- [ ] 5.1 Create `src/infrastructure/migrations/008_add_delegation_chains.sql` — create `delegation_chains` table with columns: `id` (UUID PK), `tenant_id` (UUID FK), `delegator_agent_id` (UUID FK), `delegatee_agent_id` (UUID FK), `scopes` (TEXT[]), `delegation_token` (TEXT UNIQUE), `signature` (TEXT), `ttl_seconds` (INTEGER CHECK 6086400), `issued_at` (TIMESTAMPTZ), `expires_at` (TIMESTAMPTZ), `revoked_at` (TIMESTAMPTZ nullable), `created_at` (TIMESTAMPTZ DEFAULT NOW); create all four indexes as specified in spec - [x] 5.1 Create `src/infrastructure/migrations/008_add_delegation_chains.sql` — create `delegation_chains` table with columns: `id` (UUID PK), `tenant_id` (UUID FK), `delegator_agent_id` (UUID FK), `delegatee_agent_id` (UUID FK), `scopes` (TEXT[]), `delegation_token` (TEXT UNIQUE), `signature` (TEXT), `ttl_seconds` (INTEGER CHECK 6086400), `issued_at` (TIMESTAMPTZ), `expires_at` (TIMESTAMPTZ), `revoked_at` (TIMESTAMPTZ nullable), `created_at` (TIMESTAMPTZ DEFAULT NOW); create all four indexes as specified in spec
- [ ] 5.2 Create `src/types/delegation.ts` — define interfaces: `DelegationChain`, `CreateDelegationRequest` (delegateeAgentId, scopes, ttlSeconds), `DelegationVerificationResult` (valid, chainId, delegatorAgentId, delegateeAgentId, scopes, issuedAt, expiresAt, revokedAt), `DelegationTokenPayload` - [x] 5.2 Create `src/types/delegation.ts` — define interfaces: `DelegationChain`, `CreateDelegationRequest` (delegateeAgentId, scopes, ttlSeconds), `DelegationVerificationResult` (valid, chainId, delegatorAgentId, delegateeAgentId, scopes, issuedAt, expiresAt, revokedAt), `DelegationTokenPayload`
## 6. WS2: A2A Authorization — Crypto & Service ## 6. WS2: A2A Authorization — Crypto & Service
- [ ] 6.1 Create `src/utils/delegationCrypto.ts` — implement `signDelegationPayload(payload: DelegationTokenPayload, secret: string): string` using HMAC-SHA256 (Node.js `crypto.createHmac('sha256', secret)`); implement `verifyDelegationSignature(payload: DelegationTokenPayload, signature: string, secret: string): boolean`; implement `generateDelegationToken(): string` (UUID v4); export only these three functions — no other exports - [x] 6.1 Create `src/utils/delegationCrypto.ts` — implement `signDelegationPayload(payload: DelegationTokenPayload, secret: string): string` using HMAC-SHA256 (Node.js `crypto.createHmac('sha256', secret)`); implement `verifyDelegationSignature(payload: DelegationTokenPayload, signature: string, secret: string): boolean`; implement `generateDelegationToken(): string` (UUID v4); export only these three functions — no other exports
- [ ] 6.2 Create `src/services/DelegationService.ts` — implement `IDelegationService` interface; `createDelegation`: validate delegateeAgentId exists in same tenant, validate scopes ⊆ delegator's scopes, reject self-delegation, sign payload, insert `delegation_chains` row, write audit log entry (`delegation.created`), return `DelegationChain` - [x] 6.2 Create `src/services/DelegationService.ts` — implement `IDelegationService` interface; `createDelegation`: validate delegateeAgentId exists in same tenant, validate scopes ⊆ delegator's scopes, reject self-delegation, sign payload, insert `delegation_chains` row, write audit log entry (`delegation.created`), return `DelegationChain`
- [ ] 6.3 Implement `DelegationService.verifyDelegation(delegationToken)` — fetch chain row by `delegation_token`, if not found throw `NotFoundError`, verify HMAC signature, check `expires_at > NOW()` and `revoked_at IS NULL`, return `DelegationVerificationResult` with `valid: true/false` (never throw on expired/revoked — return `valid: false`); write audit log entry (`delegation.verified`) - [x] 6.3 Implement `DelegationService.verifyDelegation(delegationToken)` — fetch chain row by `delegation_token`, if not found throw `NotFoundError`, verify HMAC signature, check `expires_at > NOW()` and `revoked_at IS NULL`, return `DelegationVerificationResult` with `valid: true/false` (never throw on expired/revoked — return `valid: false`); write audit log entry (`delegation.verified`)
- [ ] 6.4 Implement `DelegationService.revokeDelegation(chainId, requestingAgentId)` — fetch chain by ID, verify `delegator_agent_id === requestingAgentId` (else throw `ForbiddenError`), check not already revoked (else throw `ConflictError`), update `revoked_at = NOW()`, write audit log entry (`delegation.revoked`) - [x] 6.4 Implement `DelegationService.revokeDelegation(chainId, requestingAgentId)` — fetch chain by ID, verify `delegator_agent_id === requestingAgentId` (else throw `ForbiddenError`), check not already revoked (else throw `ConflictError`), update `revoked_at = NOW()`, write audit log entry (`delegation.revoked`)
## 7. WS2: A2A Authorization — Controller, Routes, Tests ## 7. WS2: A2A Authorization — Controller, Routes, Tests
- [ ] 7.1 Create `src/controllers/DelegationController.ts` — implement `createDelegation` handler (POST /oauth2/token/delegate): extract authenticated agent ID from request context, call `DelegationService.createDelegation`, return HTTP 201; implement `verifyDelegation` handler (POST /oauth2/token/verify-delegation): call `DelegationService.verifyDelegation`, return HTTP 200; implement `revokeDelegation` handler (DELETE /oauth2/token/delegate/:chainId): call `DelegationService.revokeDelegation`, return HTTP 204 - [x] 7.1 Create `src/controllers/DelegationController.ts` — implement `createDelegation` handler (POST /oauth2/token/delegate): extract authenticated agent ID from request context, call `DelegationService.createDelegation`, return HTTP 201; implement `verifyDelegation` handler (POST /oauth2/token/verify-delegation): call `DelegationService.verifyDelegation`, return HTTP 200; implement `revokeDelegation` handler (DELETE /oauth2/token/delegate/:chainId): call `DelegationService.revokeDelegation`, return HTTP 204
- [ ] 7.2 Create `src/routes/delegation.ts` — Express router registering `POST /oauth2/token/delegate`, `POST /oauth2/token/verify-delegation`, `DELETE /oauth2/token/delegate/:chainId` with authentication middleware on all three routes - [x] 7.2 Create `src/routes/delegation.ts` — Express router registering `POST /oauth2/token/delegate`, `POST /oauth2/token/verify-delegation`, `DELETE /oauth2/token/delegate/:chainId` with authentication middleware on all three routes
- [ ] 7.3 Register delegation router in `src/routes/index.ts` behind `A2A_ENABLED` feature flag — return HTTP 404 on all delegation routes when `A2A_ENABLED=false` - [x] 7.3 Register delegation router in `src/routes/index.ts` behind `A2A_ENABLED` feature flag — return HTTP 404 on all delegation routes when `A2A_ENABLED=false`
- [ ] 7.4 Add delegation Prometheus metrics: `agentidp_delegations_created_total`, `agentidp_delegations_verified_total` (labels: result), `agentidp_delegations_revoked_total` — increment in `DelegationController` handlers - [x] 7.4 Add delegation Prometheus metrics: `agentidp_delegations_created_total`, `agentidp_delegations_verified_total` (labels: result), `agentidp_delegations_revoked_total` — increment in `DelegationController` handlers
- [ ] 7.5 Add delegation endpoints to `docs/openapi.yaml` — include all request/response schemas, error responses, and authentication requirements as defined in spec - [x] 7.5 Add delegation endpoints to `docs/openapi.yaml` — include all request/response schemas, error responses, and authentication requirements as defined in spec
- [ ] 7.6 Write unit tests for `delegationCrypto.ts` — test sign/verify round-trip, test tampered payload fails verification, test different secrets produce different signatures - [x] 7.6 Write unit tests for `delegationCrypto.ts` — test sign/verify round-trip, test tampered payload fails verification, test different secrets produce different signatures
- [ ] 7.7 Write unit tests for `DelegationService` — mock DB and audit service; test: create delegation (valid), create delegation (scope escalation rejected), create delegation (self-delegation rejected), create delegation (delegatee in different tenant rejected), verify delegation (valid), verify delegation (expired — returns valid: false not throw), verify delegation (revoked — returns valid: false), revoke delegation (by delegator — succeeds), revoke delegation (by non-delegator — throws ForbiddenError), revoke delegation (already revoked — throws ConflictError) - [x] 7.7 Write unit tests for `DelegationService` — mock DB and audit service; test: create delegation (valid), create delegation (scope escalation rejected), create delegation (self-delegation rejected), create delegation (delegatee in different tenant rejected), verify delegation (valid), verify delegation (expired — returns valid: false not throw), verify delegation (revoked — returns valid: false), revoke delegation (by delegator — succeeds), revoke delegation (by non-delegator — throws ForbiddenError), revoke delegation (already revoked — throws ConflictError)
- [ ] 7.8 Write integration tests for delegation endpoints — test all happy paths and all error cases defined in spec; verify audit log entries are created for each delegation operation - [x] 7.8 Write integration tests for delegation endpoints — test all happy paths and all error cases defined in spec; verify audit log entries are created for each delegation operation
## 8. WS5: Developer Experience — Scaffold Service ## 8. WS5: Developer Experience — Scaffold Service
- [ ] 8.1 Install `archiver` and `@types/archiver` in API `package.json` - [x] 8.1 Install `archiver` and `@types/archiver` in API `package.json`
- [ ] 8.2 Create `src/types/scaffold.ts` — define `ScaffoldLanguage` union (`'typescript' | 'python' | 'go' | 'java' | 'rust'`), `ScaffoldOptions` interface, `ScaffoldTemplate` interface - [x] 8.2 Create `src/types/scaffold.ts` — define `ScaffoldLanguage` union (`'typescript' | 'python' | 'go' | 'java' | 'rust'`), `ScaffoldOptions` interface, `ScaffoldTemplate` interface
- [ ] 8.3 Create scaffold template files for TypeScript in `src/templates/scaffold/typescript/`: `package.json.tmpl`, `tsconfig.json.tmpl`, `src/index.ts.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` — each file uses `{{AGENT_ID}}`, `{{AGENT_NAME}}`, `{{CLIENT_ID}}`, `{{API_URL}}` as template variables; `.env.example.tmpl` MUST include `AGENTIDP_CLIENT_SECRET=<your-client-secret>` placeholder (never inject real secret) - [x] 8.3 Create scaffold template files for TypeScript in `src/templates/scaffold/typescript/`: `package.json.tmpl`, `tsconfig.json.tmpl`, `src/index.ts.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` — each file uses `{{AGENT_ID}}`, `{{AGENT_NAME}}`, `{{CLIENT_ID}}`, `{{API_URL}}` as template variables; `.env.example.tmpl` MUST include `AGENTIDP_CLIENT_SECRET=<your-client-secret>` placeholder (never inject real secret)
- [ ] 8.4 Create scaffold template files for Python in `src/templates/scaffold/python/`: `requirements.txt.tmpl`, `main.py.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` — same template variable convention - [x] 8.4 Create scaffold template files for Python in `src/templates/scaffold/python/`: `requirements.txt.tmpl`, `main.py.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` — same template variable convention
- [ ] 8.5 Create scaffold template files for Go in `src/templates/scaffold/go/`: `go.mod.tmpl`, `main.go.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` - [x] 8.5 Create scaffold template files for Go in `src/templates/scaffold/go/`: `go.mod.tmpl`, `main.go.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl`
- [ ] 8.6 Create scaffold template files for Java in `src/templates/scaffold/java/`: `pom.xml.tmpl`, `src/main/java/Main.java.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` - [x] 8.6 Create scaffold template files for Java in `src/templates/scaffold/java/`: `pom.xml.tmpl`, `src/main/java/Main.java.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl`
- [ ] 8.7 Create scaffold template files for Rust in `src/templates/scaffold/rust/`: `Cargo.toml.tmpl`, `src/main.rs.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl` - [x] 8.7 Create scaffold template files for Rust in `src/templates/scaffold/rust/`: `Cargo.toml.tmpl`, `src/main.rs.tmpl`, `.env.example.tmpl`, `.gitignore.tmpl`, `README.md.tmpl`
- [ ] 8.8 Create `src/services/ScaffoldService.ts` — implement `IScaffoldService`; `generateScaffold(agentId, language, apiUrl)`: load template files for language, inject template variables (replace `{{AGENT_ID}}`, `{{AGENT_NAME}}`, `{{CLIENT_ID}}`, `{{API_URL}}`), build in-memory ZIP using `archiver`; return `{ stream: NodeJS.ReadableStream, filename: string }`; emit `agentidp_scaffold_generated_total` counter and `agentidp_scaffold_generation_duration_ms` histogram - [x] 8.8 Create `src/services/ScaffoldService.ts` — implement `IScaffoldService`; `generateScaffold(agentId, language, apiUrl)`: load template files for language, inject template variables (replace `{{AGENT_ID}}`, `{{AGENT_NAME}}`, `{{CLIENT_ID}}`, `{{API_URL}}`), build in-memory ZIP using `archiver`; return `{ stream: NodeJS.ReadableStream, filename: string }`; emit `agentidp_scaffold_generated_total` counter and `agentidp_scaffold_generation_duration_ms` histogram
## 9. WS5: Developer Experience — Scaffold Controller & Route ## 9. WS5: Developer Experience — Scaffold Controller & Route
- [ ] 9.1 Create `src/controllers/ScaffoldController.ts` — implement `getScaffold` handler for `GET /sdk/scaffold/:agentId`: validate `language` query param against `ScaffoldLanguage` union (HTTP 400 on invalid); fetch agent, verify agent belongs to authenticated tenant (HTTP 403 if not); call `ScaffoldService.generateScaffold`; set `Content-Type: application/zip`, `Content-Disposition: attachment; filename="..."`, pipe stream to response; write audit log entry (`scaffold.generated`, metadata: `{ language }`) - [x] 9.1 Create `src/controllers/ScaffoldController.ts` — implement `getScaffold` handler for `GET /sdk/scaffold/:agentId`: validate `language` query param against `ScaffoldLanguage` union (HTTP 400 on invalid); fetch agent, verify agent belongs to authenticated tenant (HTTP 403 if not); call `ScaffoldService.generateScaffold`; set `Content-Type: application/zip`, `Content-Disposition: attachment; filename="..."`, pipe stream to response; write audit log entry (`scaffold.generated`, metadata: `{ language }`)
- [ ] 9.2 Create `src/routes/scaffold.ts` — Express router for `GET /sdk/scaffold/:agentId` with authentication middleware; apply scaffold-specific rate limiter (10 req/min per tenant, separate from global rate limiter) - [x] 9.2 Create `src/routes/scaffold.ts` — Express router for `GET /sdk/scaffold/:agentId` with authentication middleware; apply scaffold-specific rate limiter (10 req/min per tenant, separate from global rate limiter)
- [ ] 9.3 Register `scaffold` router in `src/routes/index.ts` - [x] 9.3 Register `scaffold` router in `src/routes/index.ts`
- [ ] 9.4 Add `GET /sdk/scaffold/:agentId` to `docs/openapi.yaml` — document binary response type, query parameters, all error responses - [x] 9.4 Add `GET /sdk/scaffold/:agentId` to `docs/openapi.yaml` — document binary response type, query parameters, all error responses
- [ ] 9.5 Write unit tests for `ScaffoldService` — test: generate TypeScript scaffold (verify ZIP contains all 6 files), generate Python scaffold (verify all 5 files), verify `{{CLIENT_ID}}` is replaced in `.env.example`, verify `{{AGENTIDP_CLIENT_SECRET}}` is placeholder not real secret, verify invalid language throws `ValidationError` - [x] 9.5 Write unit tests for `ScaffoldService` — test: generate TypeScript scaffold (verify ZIP contains all 6 files), generate Python scaffold (verify all 5 files), verify `{{CLIENT_ID}}` is replaced in `.env.example`, verify `{{AGENTIDP_CLIENT_SECRET}}` is placeholder not real secret, verify invalid language throws `ValidationError`
- [ ] 9.6 Write integration tests for scaffold endpoint — test: TypeScript scaffold returns ZIP with correct `Content-Type` and `Content-Disposition`; Python scaffold returns ZIP; HTTP 400 on invalid language; HTTP 403 when agent belongs to different tenant; HTTP 404 when agent does not exist - [x] 9.6 Write integration tests for scaffold endpoint — test: TypeScript scaffold returns ZIP with correct `Content-Type` and `Content-Disposition`; Python scaffold returns ZIP; HTTP 400 on invalid language; HTTP 403 when agent belongs to different tenant; HTTP 404 when agent does not exist
## 10. WS5: Developer Experience — Portal & CLI ## 10. WS5: Developer Experience — Portal & CLI
- [ ] 10.1 Install `@stoplight/elements` in `portal/package.json` — remove `swagger-ui-react` - [x] 10.1 Install `@stoplight/elements` in `portal/package.json` — remove `swagger-ui-react`
- [ ] 10.2 Rewrite `portal/app/api-explorer/page.tsx` — replace `SwaggerUI` component with `@stoplight/elements` `<API>` component; set `apiDescriptionUrl`, `router="hash"`, `layout="sidebar"`, `hideSchemas={false}`, `tryItCredentialsPolicy="same-origin"`; import Elements CSS; remove all Swagger UI imports and CSS - [x] 10.2 Rewrite `portal/app/api-explorer/page.tsx` — replace `SwaggerUI` component with `@stoplight/elements` `<API>` component; set `apiDescriptionUrl`, `router="hash"`, `layout="sidebar"`, `hideSchemas={false}`, `tryItCredentialsPolicy="same-origin"`; import Elements CSS; remove all Swagger UI imports and CSS
- [ ] 10.3 Run `npm run build` in `portal/` — verify zero TypeScript errors and zero ESLint errors after Elements integration - [x] 10.3 Run `npm run build` in `portal/` — verify zero TypeScript errors and zero ESLint errors after Elements integration
- [ ] 10.4 Install `unzipper` and `@types/unzipper` in `cli/package.json` - [x] 10.4 Install `unzipper` and `@types/unzipper` in `cli/package.json`
- [ ] 10.5 Create `cli/src/commands/scaffold.ts` — implement `sentryagent scaffold` command with Commander options: `--agent-id <id>` (required), `--language <lang>` (default: typescript), `--out <directory>` (default: `.`); load config, issue Bearer token, call `GET /sdk/scaffold/{agentId}?language={language}`, pipe response through `unzipper.Extract({ path: outDir })`, print success message and next steps; handle errors (404, 403, 400) with human-readable messages - [x] 10.5 Create `cli/src/commands/scaffold.ts` — implement `sentryagent scaffold` command with Commander options: `--agent-id <id>` (required), `--language <lang>` (default: typescript), `--out <directory>` (default: `.`); load config, issue Bearer token, call `GET /sdk/scaffold/{agentId}?language={language}`, pipe response through `unzipper.Extract({ path: outDir })`, print success message and next steps; handle errors (404, 403, 400) with human-readable messages
- [ ] 10.6 Register `scaffold` command in `cli/src/index.ts` — add `.addCommand(scaffoldCommand)` to Commander program - [x] 10.6 Register `scaffold` command in `cli/src/index.ts` — add `.addCommand(scaffoldCommand)` to Commander program
- [ ] 10.7 Run `npm run build` in `cli/` — zero TypeScript errors; run `node dist/index.js scaffold --help` — outputs correct usage - [x] 10.7 Run `npm run build` in `cli/` — zero TypeScript errors; run `node dist/index.js scaffold --help` — outputs correct usage
## 11. QA & Release ## 11. QA & Release
- [ ] 11.1 Run `cargo build` and `cargo clippy -- -D warnings` in `sdk-rust/` — zero warnings; run `cargo test` — all unit tests pass - [x] 11.1 Run `cargo build` and `cargo clippy -- -D warnings` in `sdk-rust/` — zero warnings; run `cargo test` — all unit tests pass
- [ ] 11.2 Run `tsc --noEmit` across API, portal, and CLI — zero TypeScript errors - [x] 11.2 Run `tsc --noEmit` across API, portal, and CLI — zero TypeScript errors
- [ ] 11.3 Run full Jest suite (`npm test`) — all unit tests pass, coverage >= 80% across all new services: `DelegationService`, `ScaffoldService` - [x] 11.3 Run full Jest suite (`npm test`) — all unit tests pass, coverage >= 80% across all new services: `DelegationService`, `ScaffoldService`
- [ ] 11.4 Run `npm run build` in `portal/` with Elements integration — zero errors; verify `/api-explorer` page renders Elements `<API>` component - [x] 11.4 Run `npm run build` in `portal/` with Elements integration — zero errors; verify `/api-explorer` page renders Elements `<API>` component
- [ ] 11.5 Run `npm run build` in `cli/` — zero errors; run `node dist/index.js scaffold --help` — shows correct options; run `node dist/index.js --help` — shows `scaffold` command listed - [x] 11.5 Run `npm run build` in `cli/` — zero errors; run `node dist/index.js scaffold --help` — shows correct options; run `node dist/index.js --help` — shows `scaffold` command listed
- [ ] 11.6 Apply database migration `008_add_delegation_chains.sql` against a test database — verify migration runs without errors and table is created with correct schema - [x] 11.6 Apply database migration `008_add_delegation_chains.sql` against a test database — verify migration runs without errors and table is created with correct schema
- [ ] 11.7 Run integration tests for all Phase 5 endpoints — delegation (create, verify, revoke), scaffold (all 5 languages) - [x] 11.7 Run integration tests for all Phase 5 endpoints — delegation (create, verify, revoke), scaffold (all 5 languages)
- [ ] 11.8 Verify feature flag: `A2A_ENABLED=false` → delegation routes return 404 - [x] 11.8 Verify feature flag: `A2A_ENABLED=false` → delegation routes return 404
- [ ] 11.9 Verify scaffold security: `GET /sdk/scaffold/:agentId` response ZIP never contains a real `client_secret` value — `.env.example` placeholder only - [x] 11.9 Verify scaffold security: `GET /sdk/scaffold/:agentId` response ZIP never contains a real `client_secret` value — `.env.example` placeholder only
- [ ] 11.10 Commit all Phase 5 work on `main` — one conventional commit per workstream: `feat(phase-5): WS1 — Rust SDK`, `feat(phase-5): WS2 — A2A Authorization`, `feat(phase-5): WS5 — Developer Experience` - [ ] 11.10 Commit all Phase 5 work on `main` — one conventional commit per workstream: `feat(phase-5): WS1 — Rust SDK`, `feat(phase-5): WS2 — A2A Authorization`, `feat(phase-5): WS5 — Developer Experience`

655
package-lock.json generated
View File

@@ -9,6 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@open-policy-agent/opa-wasm": "^1.10.0", "@open-policy-agent/opa-wasm": "^1.10.0",
"@types/archiver": "^7.0.0",
"archiver": "^7.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bull": "^4.16.5", "bull": "^4.16.5",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -773,6 +775,102 @@
"integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/load-nyc-config": { "node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1441,6 +1539,16 @@
"@noble/hashes": "^1.1.5" "@noble/hashes": "^1.1.5"
} }
}, },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/bloom": { "node_modules/@redis/bloom": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
@@ -1582,6 +1690,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/archiver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz",
"integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==",
"license": "MIT",
"dependencies": {
"@types/readdir-glob": "*"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1834,7 +1951,6 @@
"version": "20.19.37", "version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@@ -1887,6 +2003,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/readdir-glob": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
"integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/redis": { "node_modules/@types/redis": {
"version": "2.8.32", "version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz", "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
@@ -2334,7 +2459,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -2344,7 +2468,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
@@ -2370,6 +2493,63 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/archiver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
"integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
"license": "MIT",
"dependencies": {
"archiver-utils": "^5.0.2",
"async": "^3.2.4",
"buffer-crc32": "^1.0.0",
"readable-stream": "^4.0.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^3.0.0",
"zip-stream": "^6.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/archiver-utils": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
"integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
"license": "MIT",
"dependencies": {
"glob": "^10.0.0",
"graceful-fs": "^4.2.0",
"is-stream": "^2.0.1",
"lazystream": "^1.0.0",
"lodash": "^4.17.15",
"normalize-path": "^3.0.0",
"readable-stream": "^4.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/archiver-utils/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/arg": { "node_modules/arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -2407,6 +2587,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -2433,6 +2619,20 @@
"proxy-from-env": "^2.1.0" "proxy-from-env": "^2.1.0"
} }
}, },
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -2563,9 +2763,99 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.6.0.tgz",
"integrity": "sha512-2YkS7NuiJceSEbyEOdSNLE9tsGd+f4+f7C+Nik/MCk27SYdwIMPT/yRKvg++FZhQXgk0KWJKJyXX9RhVV0RGqA==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
"integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.12.0.tgz",
"integrity": "sha512-w28i8lkBgREV3rPXGbgK+BO66q+ZpKqRWrZLiCdmmUlLPrQ45CzkvRhN+7lnv00Gpi2zy5naRxnUFAxCECDm9g==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
"integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -2672,7 +2962,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -2772,6 +3061,15 @@
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/buffer-equal-constant-time": { "node_modules/buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -2987,7 +3285,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@@ -3000,7 +3297,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": { "node_modules/combined-stream": {
@@ -3025,6 +3321,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/compress-commons": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
"integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
"license": "MIT",
"dependencies": {
"crc-32": "^1.2.0",
"crc32-stream": "^6.0.0",
"is-stream": "^2.0.1",
"normalize-path": "^3.0.0",
"readable-stream": "^4.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3095,6 +3407,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.6", "version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@@ -3112,6 +3430,31 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc32-stream": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
"integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
"license": "MIT",
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^4.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/create-jest": { "node_modules/create-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@@ -3166,7 +3509,6 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@@ -3384,6 +3726,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": { "node_modules/ecdsa-sig-formatter": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -3423,7 +3771,6 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
@@ -3747,6 +4094,15 @@
"node": ">=0.8.x" "node": ">=0.8.x"
} }
}, },
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/execa": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -3865,6 +4221,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -4063,6 +4425,34 @@
} }
} }
}, },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -4356,7 +4746,6 @@
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/graphemer": { "node_modules/graphemer": {
@@ -4707,7 +5096,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4760,7 +5148,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4769,11 +5156,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
@@ -4847,6 +5239,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest": { "node_modules/jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@@ -5705,6 +6112,48 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"license": "MIT",
"dependencies": {
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 0.6.3"
}
},
"node_modules/lazystream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/lazystream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/leven": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -5992,7 +6441,6 @@
"version": "9.0.9", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.2" "brace-expansion": "^2.0.2"
@@ -6014,6 +6462,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/morgan": { "node_modules/morgan": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@@ -6221,7 +6678,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -6430,6 +6886,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6495,7 +6957,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -6508,6 +6969,28 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
@@ -6864,6 +7347,12 @@
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/process-warning": { "node_modules/process-warning": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
@@ -7053,6 +7542,27 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/real-require": { "node_modules/real-require": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@@ -7337,7 +7847,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
@@ -7350,7 +7859,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -7536,6 +8044,17 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamx": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -7563,7 +8082,21 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true, "license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@@ -7578,7 +8111,19 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true, "license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@@ -7714,6 +8259,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/tar-stream": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/tdigest": { "node_modules/tdigest": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
@@ -7723,6 +8280,15 @@
"bintrees": "1.0.2" "bintrees": "1.0.2"
} }
}, },
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/test-exclude": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -7762,6 +8328,15 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -8062,7 +8637,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
@@ -8115,6 +8689,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -8208,7 +8788,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
@@ -8255,6 +8834,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -8362,6 +8959,20 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zip-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
"integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
"license": "MIT",
"dependencies": {
"archiver-utils": "^5.0.0",
"compress-commons": "^6.0.2",
"readable-stream": "^4.0.0"
},
"engines": {
"node": ">= 14"
}
} }
} }
} }

View File

@@ -17,6 +17,8 @@
}, },
"dependencies": { "dependencies": {
"@open-policy-agent/opa-wasm": "^1.10.0", "@open-policy-agent/opa-wasm": "^1.10.0",
"@types/archiver": "^7.0.0",
"archiver": "^7.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bull": "^4.16.5", "bull": "^4.16.5",
"cors": "^2.8.5", "cors": "^2.8.5",

View File

@@ -1,38 +1,28 @@
import type React from 'react'; 'use client';
import { SwaggerExplorer } from '@/components/SwaggerExplorer';
export const metadata = { import dynamic from 'next/dynamic';
title: 'API Explorer — SentryAgent AgentIdP',
description:
'Interactively explore and test the SentryAgent AgentIdP REST API.',
};
export default function ApiExplorerPage(): React.ReactElement { // @stoplight/elements accesses `document` at module load time,
const apiUrl = // so it must be imported client-side only (ssr: false).
process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000'; const ElementsAPI = dynamic(
async () => {
await import('@stoplight/elements/styles.min.css');
const mod = await import('@stoplight/elements');
return mod.API;
},
{ ssr: false },
);
export default function ApiExplorerPage() {
return ( return (
<div className="px-4 py-8"> <main className="h-screen w-full">
<div className="mx-auto max-w-7xl"> <ElementsAPI
<div className="mb-8"> apiDescriptionUrl={`${process.env.NEXT_PUBLIC_API_URL}/openapi.json`}
<h1 className="text-3xl font-extrabold text-slate-900"> router="hash"
API Explorer layout="sidebar"
</h1> hideSchemas={false}
<p className="mt-2 text-slate-600"> tryItCredentialsPolicy="same-origin"
Explore, authenticate, and test every AgentIdP endpoint directly />
from your browser. Use the Authorize button to set your Bearer </main>
token.
</p>
<p className="mt-1 text-sm text-slate-400">
Spec loaded from:{' '}
<code className="rounded bg-slate-100 px-1.5 py-0.5 text-xs">
{apiUrl}/openapi.json
</code>
</p>
</div>
<SwaggerExplorer apiUrl={apiUrl} />
</div>
</div>
); );
} }

5406
portal/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,10 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@stoplight/elements": "^9.0.16",
"next": "14.2.5", "next": "14.2.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1"
"swagger-ui-react": "^5.17.14"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.0", "@types/node": "^20.14.0",

29
portal/types/stoplight-elements.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
/**
* Type declaration for @stoplight/elements.
* Required because the package's `exports` field does not expose types correctly
* under `moduleResolution: "bundler"`.
*/
declare module '@stoplight/elements' {
import type React from 'react';
export interface APIProps {
/** URL of the OpenAPI description (YAML or JSON). */
apiDescriptionUrl: string;
/** Routing strategy. Use "hash" for static hosting. */
router?: 'hash' | 'memory' | 'history' | 'static';
/** Layout variant. */
layout?: 'sidebar' | 'stacked';
/** Whether to hide schema definitions. */
hideSchemas?: boolean;
/** Credentials policy for the Try It panel. */
tryItCredentialsPolicy?: 'omit' | 'same-origin' | 'include';
}
export const API: React.ComponentType<APIProps>;
}
declare module '@stoplight/elements/styles.min.css' {
const content: string;
export default content;
}

View File

@@ -68,6 +68,12 @@ import { createOIDCRouter } from './routes/oidc.js';
import { createFederationRouter } from './routes/federation.js'; import { createFederationRouter } from './routes/federation.js';
import { createWebhooksRouter } from './routes/webhooks.js'; import { createWebhooksRouter } from './routes/webhooks.js';
import { createComplianceRouter } from './routes/compliance.js'; import { createComplianceRouter } from './routes/compliance.js';
import { createDelegationRouter } from './routes/delegation.js';
import { DelegationService } from './services/DelegationService.js';
import { DelegationController } from './controllers/DelegationController.js';
import { createScaffoldRouter } from './routes/scaffold.js';
import { ScaffoldService } from './services/ScaffoldService.js';
import { ScaffoldController } from './controllers/ScaffoldController.js';
import { errorHandler } from './middleware/errorHandler.js'; import { errorHandler } from './middleware/errorHandler.js';
import { createOpaMiddleware } from './middleware/opa.js'; import { createOpaMiddleware } from './middleware/opa.js';
@@ -326,6 +332,22 @@ export async function createApp(): Promise<Application> {
app.use(`${API_BASE}/oidc`, createOIDCTrustPoliciesRouter(oidcTrustPolicyController, authMiddleware)); app.use(`${API_BASE}/oidc`, createOIDCTrustPoliciesRouter(oidcTrustPolicyController, authMiddleware));
app.use(`${API_BASE}/oidc`, createOIDCTokenExchangeRouter(oidcTokenExchangeController)); app.use(`${API_BASE}/oidc`, createOIDCTokenExchangeRouter(oidcTokenExchangeController));
// ────────────────────────────────────────────────────────────────
// Phase 5 WS2: A2A Delegation (guarded by A2A_ENABLED flag)
// ────────────────────────────────────────────────────────────────
if (process.env['A2A_ENABLED'] !== 'false') {
const delegationService = new DelegationService(pool, auditService);
const delegationController = new DelegationController(delegationService);
app.use(`${API_BASE}`, createDelegationRouter(delegationController, authMiddleware));
}
// ────────────────────────────────────────────────────────────────
// Phase 5 WS5: Scaffold Generator
// ────────────────────────────────────────────────────────────────
const scaffoldService = new ScaffoldService();
const scaffoldController = new ScaffoldController(scaffoldService, pool, auditService);
app.use(`${API_BASE}`, createScaffoldRouter(scaffoldController, authMiddleware));
// ──────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────
// Dashboard static assets (served from dashboard/dist/) // Dashboard static assets (served from dashboard/dist/)
// Placed after API routes so API routes take precedence. // Placed after API routes so API routes take precedence.

View File

@@ -0,0 +1,114 @@
/**
* Scaffold Controller for SentryAgent.ai AgentIdP.
* HTTP handler for the GET /sdk/scaffold/:agentId endpoint.
*/
import { Request, Response, NextFunction } from 'express';
import { Pool } from 'pg';
import { AuditService } from '../services/AuditService.js';
import { ScaffoldService, SCAFFOLD_LANGUAGES } from '../services/ScaffoldService.js';
import { ScaffoldLanguage } from '../types/scaffold.js';
import { AgentNotFoundError, AuthorizationError, ValidationError } from '../utils/errors.js';
/**
* Controller for the scaffold generator endpoint.
* Validates request, fetches agent credentials, delegates ZIP generation to ScaffoldService.
*/
export class ScaffoldController {
/**
* @param scaffoldService - The scaffold generation service.
* @param pool - PostgreSQL connection pool for agent credential lookup.
* @param auditService - Audit log service.
*/
constructor(
private readonly scaffoldService: ScaffoldService,
private readonly pool: Pool,
private readonly auditService: AuditService,
) {}
/**
* Handles GET /sdk/scaffold/:agentId — generates and streams a scaffold ZIP.
*
* @param req - Express request with agentId path param and language query param.
* @param res - Express response streaming the ZIP file.
* @param next - Express next function.
*/
getScaffold = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
if (!req.user) {
throw new AuthorizationError();
}
const { agentId } = req.params;
const rawLanguage = req.query['language'] as string | undefined;
const language: ScaffoldLanguage = (rawLanguage as ScaffoldLanguage) ?? 'typescript';
if (!SCAFFOLD_LANGUAGES.includes(language)) {
throw new ValidationError(
`Unsupported language '${language}'. Choose: ${SCAFFOLD_LANGUAGES.join(', ')}`,
{ code: 'INVALID_LANGUAGE' },
);
}
const tenantId = req.user.organization_id ?? 'org_system';
// Fetch agent and verify it belongs to the authenticated tenant
const agentResult = await this.pool.query<{
agent_id: string;
email: string;
organization_id: string;
}>(
`SELECT agent_id, email, organization_id FROM agents WHERE agent_id = $1`,
[agentId],
);
if (agentResult.rows.length === 0) {
throw new AgentNotFoundError(agentId);
}
const agentRow = agentResult.rows[0];
if (agentRow.organization_id !== tenantId) {
throw new AuthorizationError('You do not own this agent.');
}
// Fetch the agent's active credential client_id
const credResult = await this.pool.query<{ client_id: string }>(
`SELECT client_id FROM credentials WHERE agent_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1`,
[agentId],
);
const clientId =
credResult.rows.length > 0 ? credResult.rows[0].client_id : agentRow.agent_id;
const apiUrl = process.env['API_URL'] ?? process.env['NEXT_PUBLIC_API_URL'] ?? 'https://api.sentryagent.ai';
const { stream, filename } = await this.scaffoldService.generateScaffold({
agentId,
agentName: agentRow.email.split('@')[0] ?? agentId,
clientId,
language,
apiUrl,
});
const ipAddress = req.ip ?? '0.0.0.0';
const userAgent = req.headers['user-agent'] ?? 'unknown';
await this.auditService.logEvent(
req.user.sub,
'scaffold.generated',
'success',
ipAddress,
userAgent,
{ agentId, language },
tenantId,
);
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
stream.pipe(res);
} catch (err) {
next(err);
}
};
}

View File

@@ -172,3 +172,66 @@ export const billingLimitRejectionsTotal = new Counter({
labelNames: ['tenant_id', 'limit_type'] as const, labelNames: ['tenant_id', 'limit_type'] as const,
registers: [metricsRegistry], registers: [metricsRegistry],
}); });
// ────────────────────────────────────────────────────────────────
// Phase 5 — WS2: A2A Delegation Metrics
// ────────────────────────────────────────────────────────────────
/**
* Total number of A2A delegation chains created.
* Labels: tenant_id
*/
export const delegationsCreatedTotal = new Counter({
name: 'agentidp_delegations_created_total',
help: 'Total number of A2A delegation chains created.',
labelNames: ['tenant_id'] as const,
registers: [metricsRegistry],
});
/**
* Total number of A2A delegation verifications performed.
* Labels: tenant_id, result (valid | invalid | expired | revoked)
*/
export const delegationsVerifiedTotal = new Counter({
name: 'agentidp_delegations_verified_total',
help: 'Total number of A2A delegation verifications, labelled by outcome.',
labelNames: ['tenant_id', 'result'] as const,
registers: [metricsRegistry],
});
/**
* Total number of A2A delegation chains revoked.
* Labels: tenant_id
*/
export const delegationsRevokedTotal = new Counter({
name: 'agentidp_delegations_revoked_total',
help: 'Total number of A2A delegation chains revoked.',
labelNames: ['tenant_id'] as const,
registers: [metricsRegistry],
});
// ────────────────────────────────────────────────────────────────
// Phase 5 — WS5: Scaffold Metrics
// ────────────────────────────────────────────────────────────────
/**
* Total number of scaffold ZIPs generated, labelled by language.
*/
export const scaffoldGeneratedTotal = new Counter({
name: 'agentidp_scaffold_generated_total',
help: 'Total number of scaffold ZIPs generated by target language.',
labelNames: ['language'] as const,
registers: [metricsRegistry],
});
/**
* Duration of scaffold ZIP generation in milliseconds.
* Labels: language
*/
export const scaffoldGenerationDurationMs = new Histogram({
name: 'agentidp_scaffold_generation_duration_ms',
help: 'Time taken to generate a scaffold ZIP archive in milliseconds.',
labelNames: ['language'] as const,
buckets: [10, 50, 100, 250, 500, 1000, 2500],
registers: [metricsRegistry],
});

58
src/routes/scaffold.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* Scaffold routes for SentryAgent.ai AgentIdP.
* Provides the GET /sdk/scaffold/:agentId endpoint.
*/
import { Router, RequestHandler, Request, Response, NextFunction } from 'express';
import { RateLimiterMemory } from 'rate-limiter-flexible';
import { ScaffoldController } from '../controllers/ScaffoldController.js';
import { asyncHandler } from '../utils/asyncHandler.js';
import { RateLimitError } from '../utils/errors.js';
/** Scaffold-specific rate limiter: 10 requests per minute per tenant. */
const scaffoldRateLimiter = new RateLimiterMemory({ points: 10, duration: 60 });
/**
* Express middleware enforcing the scaffold-specific rate limit by tenant ID.
*/
async function scaffoldRateLimitMiddleware(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
const tenantId = req.user?.organization_id ?? req.ip ?? 'unknown';
try {
await scaffoldRateLimiter.consume(tenantId);
next();
} catch {
const retryAfter = Math.ceil(60);
res.setHeader('Retry-After', String(retryAfter));
next(new RateLimitError());
}
}
/**
* Creates and returns the Express router for scaffold endpoints.
*
* Routes:
* GET /sdk/scaffold/:agentId — authenticated; generate and stream a scaffold ZIP
*
* @param controller - The scaffold controller instance.
* @param authMiddleware - The JWT authentication middleware.
* @returns Configured Express router.
*/
export function createScaffoldRouter(
controller: ScaffoldController,
authMiddleware: RequestHandler,
): Router {
const router = Router();
router.get(
'/sdk/scaffold/:agentId',
authMiddleware,
asyncHandler(scaffoldRateLimitMiddleware),
asyncHandler(controller.getScaffold.bind(controller)),
);
return router;
}

View File

@@ -0,0 +1,153 @@
/**
* Scaffold Service for SentryAgent.ai AgentIdP.
* Generates in-memory ZIP archives containing language-specific starter projects
* pre-wired with the requesting agent's credentials.
*/
import archiver from 'archiver';
import { PassThrough } from 'stream';
import { readFileSync } from 'fs';
import { join } from 'path';
import { ScaffoldLanguage, ScaffoldOptions } from '../types/scaffold.js';
import { ValidationError } from '../utils/errors.js';
import {
scaffoldGeneratedTotal,
scaffoldGenerationDurationMs,
} from '../metrics/registry.js';
/** Map of language → list of template file paths (relative to the language directory). */
const TEMPLATE_FILES: Record<ScaffoldLanguage, string[]> = {
typescript: [
'package.json.tmpl',
'tsconfig.json.tmpl',
'src/index.ts.tmpl',
'.env.example.tmpl',
'.gitignore.tmpl',
'README.md.tmpl',
],
python: [
'requirements.txt.tmpl',
'main.py.tmpl',
'.env.example.tmpl',
'.gitignore.tmpl',
'README.md.tmpl',
],
go: [
'go.mod.tmpl',
'main.go.tmpl',
'.env.example.tmpl',
'.gitignore.tmpl',
'README.md.tmpl',
],
java: [
'pom.xml.tmpl',
'src/main/java/Main.java.tmpl',
'.env.example.tmpl',
'.gitignore.tmpl',
'README.md.tmpl',
],
rust: [
'Cargo.toml.tmpl',
'src/main.rs.tmpl',
'.env.example.tmpl',
'.gitignore.tmpl',
'README.md.tmpl',
],
};
/** Valid scaffold language values for input validation. */
export const SCAFFOLD_LANGUAGES: ScaffoldLanguage[] = [
'typescript',
'python',
'go',
'java',
'rust',
];
/** Interface contract for the scaffold service. */
export interface IScaffoldService {
/**
* Generate an in-memory ZIP archive for the given agent and language.
* Template variables injected: {{AGENT_ID}}, {{AGENT_NAME}}, {{CLIENT_ID}}, {{API_URL}}
*
* @param options - Scaffold generation options.
* @returns Readable stream of the ZIP binary and the filename for Content-Disposition.
*/
generateScaffold(options: ScaffoldOptions): Promise<{ stream: NodeJS.ReadableStream; filename: string }>;
}
/**
* Implementation of IScaffoldService.
* Generates scaffold ZIPs in memory using the `archiver` library — no disk writes.
*/
export class ScaffoldService implements IScaffoldService {
private readonly templatesDir: string;
constructor() {
// __dirname is available because the project compiles to CommonJS
this.templatesDir = join(__dirname, '..', 'templates', 'scaffold');
}
/**
* Generates an in-memory ZIP scaffold for the given agent.
*
* @param options - Agent metadata and target language.
* @returns Object with a readable stream of the ZIP and the ZIP filename.
* @throws ValidationError if the language is not supported.
*/
async generateScaffold(
options: ScaffoldOptions,
): Promise<{ stream: NodeJS.ReadableStream; filename: string }> {
const { agentId, agentName, clientId, language, apiUrl } = options;
if (!SCAFFOLD_LANGUAGES.includes(language)) {
throw new ValidationError(
`Unsupported language '${language}'. Choose: ${SCAFFOLD_LANGUAGES.join(', ')}`,
{ code: 'INVALID_LANGUAGE' },
);
}
const startMs = Date.now();
const safeAgentName = agentName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
const projectDir = `sentryagent-scaffold-${safeAgentName}-${language}`;
const filename = `${projectDir}.zip`;
const archive = archiver('zip', { zlib: { level: 6 } });
const passThrough = new PassThrough();
archive.pipe(passThrough);
const templateFiles = TEMPLATE_FILES[language];
for (const templateFile of templateFiles) {
const templatePath = join(this.templatesDir, language, templateFile);
const rawContent = readFileSync(templatePath, 'utf-8');
const content = this.injectVariables(rawContent, { agentId, agentName, clientId, apiUrl });
// Strip .tmpl extension for the archive entry
const archiveEntry = templateFile.endsWith('.tmpl')
? templateFile.slice(0, -5)
: templateFile;
archive.append(content, { name: `${projectDir}/${archiveEntry}` });
}
archive.finalize().catch((err: unknown) => {
passThrough.destroy(err instanceof Error ? err : new Error(String(err)));
});
const durationMs = Date.now() - startMs;
scaffoldGeneratedTotal.labels(language).inc();
scaffoldGenerationDurationMs.labels(language).observe(durationMs);
return { stream: passThrough, filename };
}
/** Replaces all template variable placeholders in a template string. */
private injectVariables(
template: string,
vars: { agentId: string; agentName: string; clientId: string; apiUrl: string },
): string {
return template
.replace(/\{\{AGENT_ID\}\}/g, vars.agentId)
.replace(/\{\{AGENT_NAME\}\}/g, vars.agentName)
.replace(/\{\{CLIENT_ID\}\}/g, vars.clientId)
.replace(/\{\{API_URL\}\}/g, vars.apiUrl);
}
}

View File

@@ -0,0 +1,3 @@
.env
{{AGENT_NAME}}
*.exe

View File

@@ -0,0 +1,26 @@
# {{AGENT_NAME}}
A SentryAgent.ai agent starter project (Go).
**Agent ID:** `{{AGENT_ID}}`
## Quick Start
1. Copy the environment file and fill in your client secret:
```bash
cp .env.example .env
# Edit .env and set AGENTIDP_CLIENT_SECRET
```
2. Install dependencies:
```bash
go mod tidy
```
3. Run the agent:
```bash
go run main.go
```

View File

@@ -0,0 +1,7 @@
module {{AGENT_NAME}}
go 1.21
require (
github.com/joho/godotenv v1.5.1
)

View File

@@ -0,0 +1,73 @@
// {{AGENT_NAME}} — SentryAgent.ai agent starter
// Agent ID: {{AGENT_ID}}
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/joho/godotenv"
)
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
func main() {
_ = godotenv.Load()
apiURL := getEnv("AGENTIDP_API_URL", "{{API_URL}}")
clientID := os.Getenv("AGENTIDP_CLIENT_ID")
clientSecret := os.Getenv("AGENTIDP_CLIENT_SECRET")
if clientID == "" || clientSecret == "" {
fmt.Fprintln(os.Stderr, "Error: AGENTIDP_CLIENT_ID and AGENTIDP_CLIENT_SECRET must be set")
os.Exit(1)
}
fmt.Printf("Issuing token for agent {{AGENT_ID}} at %s ...\n", apiURL)
form := url.Values{}
form.Set("grant_type", "client_credentials")
form.Set("client_id", clientID)
form.Set("client_secret", clientSecret)
form.Set("scope", "agents:read")
resp, err := http.Post(apiURL+"/api/v1/token", "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
if err != nil {
fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Fprintf(os.Stderr, "Token issuance failed: HTTP %d\n", resp.StatusCode)
os.Exit(1)
}
var token TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
fmt.Fprintf(os.Stderr, "Failed to decode response: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ Token issued successfully!")
fmt.Printf(" Expires in: %ds\n", token.ExpiresIn)
truncated := token.AccessToken
if len(truncated) > 20 {
truncated = truncated[:20]
}
fmt.Printf(" Token (first 20 chars): %s...\n", truncated)
}
func getEnv(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}

View File

@@ -0,0 +1,3 @@
.env
target/
*.class

View File

@@ -0,0 +1,20 @@
# {{AGENT_NAME}}
A SentryAgent.ai agent starter project (Java).
**Agent ID:** `{{AGENT_ID}}`
## Quick Start
1. Copy the environment file and fill in your client secret:
```bash
cp .env.example .env
# Edit .env and set AGENTIDP_CLIENT_SECRET
```
2. Build and run:
```bash
mvn compile exec:java -Dexec.mainClass="Main"
```

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ai.sentryagent</groupId>
<artifactId>{{AGENT_NAME}}</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,50 @@
import io.github.cdimascio.dotenv.Dotenv;
import okhttp3.*;
import com.google.gson.*;
/**
* {{AGENT_NAME}} — SentryAgent.ai agent starter (Java)
* Agent ID: {{AGENT_ID}}
*/
public class Main {
public static void main(String[] args) throws Exception {
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
String apiUrl = dotenv.get("AGENTIDP_API_URL", "{{API_URL}}");
String clientId = dotenv.get("AGENTIDP_CLIENT_ID", "");
String clientSecret = dotenv.get("AGENTIDP_CLIENT_SECRET", "");
if (clientId.isEmpty() || clientSecret.isEmpty()) {
System.err.println("Error: AGENTIDP_CLIENT_ID and AGENTIDP_CLIENT_SECRET must be set");
System.exit(1);
}
System.out.println("Issuing token for agent {{AGENT_ID}} at " + apiUrl + " ...");
OkHttpClient client = new OkHttpClient();
RequestBody body = new FormBody.Builder()
.add("grant_type", "client_credentials")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("scope", "agents:read")
.build();
Request request = new Request.Builder()
.url(apiUrl + "/api/v1/token")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.err.println("Token issuance failed: HTTP " + response.code());
System.exit(1);
}
JsonObject token = JsonParser.parseString(response.body().string()).getAsJsonObject();
String accessToken = token.get("access_token").getAsString();
int expiresIn = token.get("expires_in").getAsInt();
System.out.println("✓ Token issued successfully!");
System.out.println(" Expires in: " + expiresIn + "s");
System.out.println(" Token (first 20 chars): " + accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
}
}
}

View File

@@ -0,0 +1,6 @@
.env
__pycache__/
*.pyc
*.pyo
.venv/
venv/

View File

@@ -0,0 +1,26 @@
# {{AGENT_NAME}}
A SentryAgent.ai agent starter project (Python).
**Agent ID:** `{{AGENT_ID}}`
## Quick Start
1. Copy the environment file and fill in your client secret:
```bash
cp .env.example .env
# Edit .env and set AGENTIDP_CLIENT_SECRET
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Run the agent:
```bash
python main.py
```

View File

@@ -0,0 +1,43 @@
"""
{{AGENT_NAME}} — SentryAgent.ai agent starter
Agent ID: {{AGENT_ID}}
Demonstrates how to authenticate with SentryAgent.ai and issue an OAuth2 access token.
"""
import os
import sys
import requests
from dotenv import load_dotenv
load_dotenv()
API_URL = os.environ.get("AGENTIDP_API_URL", "{{API_URL}}")
CLIENT_ID = os.environ.get("AGENTIDP_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("AGENTIDP_CLIENT_SECRET", "")
if not CLIENT_ID or not CLIENT_SECRET:
print("Error: AGENTIDP_CLIENT_ID and AGENTIDP_CLIENT_SECRET must be set in .env")
sys.exit(1)
print(f"Issuing token for agent {{AGENT_ID}} at {API_URL} ...")
response = requests.post(
f"{API_URL}/api/v1/token",
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "agents:read",
},
)
if not response.ok:
print(f"Token issuance failed: {response.text}")
sys.exit(1)
token = response.json()
print("✓ Token issued successfully!")
print(f" Token type: Bearer")
print(f" Expires in: {token['expires_in']}s")
print(f" Token (first 20 chars): {token['access_token'][:20]}...")

View File

@@ -0,0 +1,2 @@
python-dotenv>=1.0.0
requests>=2.31.0

View File

@@ -0,0 +1,3 @@
.env
target/
Cargo.lock

View File

@@ -0,0 +1,11 @@
[package]
name = "{{AGENT_NAME}}"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.35", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenvy = "0.15"

View File

@@ -0,0 +1,20 @@
# {{AGENT_NAME}}
A SentryAgent.ai agent starter project (Rust).
**Agent ID:** `{{AGENT_ID}}`
## Quick Start
1. Copy the environment file and fill in your client secret:
```bash
cp .env.example .env
# Edit .env and set AGENTIDP_CLIENT_SECRET
```
2. Run the agent:
```bash
cargo run
```

View File

@@ -0,0 +1,47 @@
//! {{AGENT_NAME}} — SentryAgent.ai agent starter (Rust)
//! Agent ID: {{AGENT_ID}}
use std::collections::HashMap;
#[derive(serde::Deserialize)]
struct TokenResponse {
access_token: String,
expires_in: u64,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_url = std::env::var("AGENTIDP_API_URL").unwrap_or_else(|_| "{{API_URL}}".to_string());
let client_id = std::env::var("AGENTIDP_CLIENT_ID")?;
let client_secret = std::env::var("AGENTIDP_CLIENT_SECRET")?;
println!("Issuing token for agent {{AGENT_ID}} at {} ...", api_url);
let client = reqwest::Client::new();
let mut params = HashMap::new();
params.insert("grant_type", "client_credentials");
params.insert("client_id", &client_id);
params.insert("client_secret", &client_secret);
params.insert("scope", "agents:read");
let response = client
.post(format!("{}/api/v1/token", api_url))
.form(&params)
.send()
.await?;
if !response.status().is_success() {
eprintln!("Token issuance failed: HTTP {}", response.status());
std::process::exit(1);
}
let token: TokenResponse = response.json().await?;
println!("✓ Token issued successfully!");
println!(" Expires in: {}s", token.expires_in);
let truncated = &token.access_token[..token.access_token.len().min(20)];
println!(" Token (first 20 chars): {}...", truncated);
Ok(())
}

View File

@@ -0,0 +1,4 @@
.env
node_modules/
dist/
*.js.map

View File

@@ -0,0 +1,40 @@
# {{AGENT_NAME}}
A SentryAgent.ai agent starter project.
**Agent ID:** `{{AGENT_ID}}`
**API:** {{API_URL}}
## Quick Start
1. Copy the example environment file and fill in your client secret:
```bash
cp .env.example .env
# Edit .env and set AGENTIDP_CLIENT_SECRET to your agent's secret
```
2. Install dependencies:
```bash
npm install
```
3. Run the agent:
```bash
npm start
```
## Configuration
| Variable | Description |
|---|---|
| `AGENTIDP_API_URL` | SentryAgent.ai API base URL |
| `AGENTIDP_CLIENT_ID` | Your agent's OAuth2 client ID (pre-filled) |
| `AGENTIDP_CLIENT_SECRET` | Your agent's client secret (copy from dashboard) |
## Resources
- [SentryAgent.ai Documentation]({{API_URL}}/docs)
- [API Reference]({{API_URL}}/api-explorer)

View File

@@ -0,0 +1,20 @@
{
"name": "{{AGENT_NAME}}",
"version": "0.1.0",
"description": "SentryAgent.ai agent — {{AGENT_NAME}}",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"dependencies": {
"sentryagent-idp-sdk": "^1.0.0",
"dotenv": "^16.0.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.0.0",
"ts-node": "^10.9.0"
}
}

View File

@@ -0,0 +1,50 @@
import 'dotenv/config';
/**
* {{AGENT_NAME}} — SentryAgent.ai agent starter
* Agent ID: {{AGENT_ID}}
*
* This file demonstrates how to authenticate with SentryAgent.ai and issue
* an OAuth2 access token using your agent credentials.
*/
async function main(): Promise<void> {
const apiUrl = process.env['AGENTIDP_API_URL'] ?? '{{API_URL}}';
const clientId = process.env['AGENTIDP_CLIENT_ID'] ?? '';
const clientSecret = process.env['AGENTIDP_CLIENT_SECRET'] ?? '';
if (!clientId || !clientSecret) {
console.error('Error: AGENTIDP_CLIENT_ID and AGENTIDP_CLIENT_SECRET must be set in .env');
process.exit(1);
}
console.log(`Issuing token for agent {{AGENT_ID}} at ${apiUrl} ...`);
const response = await fetch(`${apiUrl}/api/v1/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: 'agents:read',
}),
});
if (!response.ok) {
const error = await response.text();
console.error('Token issuance failed:', error);
process.exit(1);
}
const token = await response.json() as { access_token: string; expires_in: number };
console.log('✓ Token issued successfully!');
console.log(` Token type: Bearer`);
console.log(` Expires in: ${token.expires_in}s`);
console.log(` Token (first 20 chars): ${token.access_token.substring(0, 20)}...`);
}
main().catch((err: unknown) => {
console.error('Unexpected error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -57,7 +57,11 @@ export type AuditAction =
| 'org.member_added' | 'org.member_added'
| 'webhook.created' | 'webhook.created'
| 'webhook.updated' | 'webhook.updated'
| 'webhook.deleted'; | 'webhook.deleted'
| 'delegation.created'
| 'delegation.verified'
| 'delegation.revoked'
| 'scaffold.generated';
/** Outcome of an audited action. */ /** Outcome of an audited action. */
export type AuditOutcome = 'success' | 'failure'; export type AuditOutcome = 'success' | 'failure';

28
src/types/scaffold.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* TypeScript types for the scaffold generator (WS5 Developer Experience).
*/
/** Supported target languages for scaffold generation. */
export type ScaffoldLanguage = 'typescript' | 'python' | 'go' | 'java' | 'rust';
/** Options for generating a scaffold project. */
export interface ScaffoldOptions {
/** Agent UUID. */
agentId: string;
/** Human-readable agent name (used in filenames and README). */
agentName: string;
/** OAuth2 client ID pre-filled in .env.example. */
clientId: string;
/** Target programming language. */
language: ScaffoldLanguage;
/** API base URL injected into template files. */
apiUrl: string;
}
/** A single file within a scaffold template. */
export interface ScaffoldTemplate {
/** Path within the ZIP archive (relative to the project root directory). */
archivePath: string;
/** File content with template variables replaced. */
content: string;
}

View File

@@ -0,0 +1,148 @@
/**
* Integration tests for the scaffold endpoint.
* Tests GET /sdk/scaffold/:agentId
*/
import request from 'supertest';
import express from 'express';
import { Pool } from 'pg';
import { AuditService } from '../../src/services/AuditService';
import { ScaffoldService } from '../../src/services/ScaffoldService';
import { ScaffoldController } from '../../src/controllers/ScaffoldController';
import { createScaffoldRouter } from '../../src/routes/scaffold';
jest.mock('../../src/services/AuditService');
const MockAuditService = AuditService as jest.MockedClass<typeof AuditService>;
const mockQuery = jest.fn();
const mockPool = { query: mockQuery } as unknown as Pool;
const AGENT_ID = 'agent-uuid-integration-test';
const TENANT_ID = 'org_system';
const CLIENT_ID = 'credential-client-id';
// Auth middleware injecting a test user that owns the agent
const testAuth = (req: express.Request, _res: express.Response, next: express.NextFunction) => {
req.user = {
sub: AGENT_ID,
client_id: CLIENT_ID,
scope: 'agents:read',
jti: 'jti-test',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
organization_id: TENANT_ID,
};
next();
};
// Auth middleware for a different tenant (forbidden)
const otherTenantAuth = (req: express.Request, _res: express.Response, next: express.NextFunction) => {
req.user = {
sub: 'other-agent-uuid',
client_id: 'other-client',
scope: 'agents:read',
jti: 'jti-other',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
organization_id: 'other-org',
};
next();
};
function buildApp(authMiddleware = testAuth) {
const app = express();
app.use(express.json());
const auditService = new MockAuditService({} as never) as jest.Mocked<AuditService>;
auditService.logEvent = jest.fn().mockResolvedValue({});
const scaffoldService = new ScaffoldService();
const controller = new ScaffoldController(scaffoldService, mockPool, auditService);
app.use('/api/v1', createScaffoldRouter(controller, authMiddleware));
app.use(
(
err: { httpStatus?: number; code?: string; message?: string },
_req: express.Request,
res: express.Response,
_next: express.NextFunction,
) => {
res.status(err.httpStatus ?? 500).json({ code: err.code ?? 'ERROR', message: err.message });
},
);
return app;
}
describe('Scaffold Endpoint', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env['API_URL'] = 'https://api.sentryagent.ai';
});
afterEach(() => {
delete process.env['API_URL'];
});
describe('GET /api/v1/sdk/scaffold/:agentId', () => {
it('returns a TypeScript scaffold ZIP with correct Content-Type and Content-Disposition', async () => {
// Agent exists and belongs to tenant
mockQuery
.mockResolvedValueOnce({
rows: [{ agent_id: AGENT_ID, email: 'my-agent@test.com', organization_id: TENANT_ID }],
})
// Credential lookup
.mockResolvedValueOnce({ rows: [{ client_id: CLIENT_ID }] });
const app = buildApp();
const res = await request(app).get(`/api/v1/sdk/scaffold/${AGENT_ID}?language=typescript`);
expect(res.status).toBe(200);
expect(res.headers['content-type']).toBe('application/zip');
expect(res.headers['content-disposition']).toMatch(/attachment; filename=".*typescript\.zip"/);
// Response body should be a non-empty buffer (ZIP magic bytes PK)
expect(res.body).toBeDefined();
});
it('returns a Python scaffold ZIP', async () => {
mockQuery
.mockResolvedValueOnce({
rows: [{ agent_id: AGENT_ID, email: 'my-agent@test.com', organization_id: TENANT_ID }],
})
.mockResolvedValueOnce({ rows: [{ client_id: CLIENT_ID }] });
const app = buildApp();
const res = await request(app).get(`/api/v1/sdk/scaffold/${AGENT_ID}?language=python`);
expect(res.status).toBe(200);
expect(res.headers['content-type']).toBe('application/zip');
});
it('returns HTTP 400 for an invalid language', async () => {
const app = buildApp();
const res = await request(app).get(`/api/v1/sdk/scaffold/${AGENT_ID}?language=cobol`);
expect(res.status).toBe(400);
});
it('returns HTTP 404 when agent does not exist', async () => {
mockQuery.mockResolvedValueOnce({ rows: [] });
const app = buildApp();
const res = await request(app).get(`/api/v1/sdk/scaffold/${AGENT_ID}?language=typescript`);
expect(res.status).toBe(404);
});
it('returns HTTP 403 when agent belongs to a different tenant', async () => {
mockQuery.mockResolvedValueOnce({
rows: [{ agent_id: AGENT_ID, email: 'my-agent@test.com', organization_id: TENANT_ID }],
});
const app = buildApp(otherTenantAuth);
const res = await request(app).get(`/api/v1/sdk/scaffold/${AGENT_ID}?language=typescript`);
expect(res.status).toBe(403);
});
});
});

View File

@@ -0,0 +1,75 @@
/**
* Error-path tests for ScaffoldService — tests the archiver finalize error handler.
* These tests use jest.mock('archiver') to simulate archiver failures.
*/
import { PassThrough } from 'stream';
// Mock archiver before importing ScaffoldService
const mockFinalize = jest.fn();
const mockArchive = {
pipe: jest.fn(),
append: jest.fn(),
finalize: mockFinalize,
};
jest.mock('archiver', () => jest.fn(() => mockArchive));
// Mock metrics to avoid registry conflicts
jest.mock('../../../src/metrics/registry', () => ({
scaffoldGeneratedTotal: { labels: jest.fn().mockReturnValue({ inc: jest.fn() }) },
scaffoldGenerationDurationMs: { labels: jest.fn().mockReturnValue({ observe: jest.fn() }) },
}));
import { ScaffoldService } from '../../../src/services/ScaffoldService';
const BASE_OPTIONS = {
agentId: 'agent-uuid-1234',
agentName: 'my-test-agent',
clientId: 'client-id-5678',
apiUrl: 'https://api.sentryagent.ai',
};
describe('ScaffoldService — archiver error path', () => {
let service: ScaffoldService;
beforeEach(() => {
jest.clearAllMocks();
// Reset pipe to actually pipe to the PassThrough so the stream reference is correct
mockArchive.pipe.mockImplementation(() => {});
service = new ScaffoldService();
});
it('destroys the PassThrough stream when archiver finalize rejects with an Error', async () => {
const archiverError = new Error('archiver finalize failure');
mockFinalize.mockReturnValue(Promise.reject(archiverError));
const { stream } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'typescript',
});
// Attach error listener to suppress unhandled error event from stream.destroy(err)
await new Promise<void>((resolve) => {
stream.on('error', () => resolve());
});
expect((stream as PassThrough).destroyed).toBe(true);
});
it('destroys the PassThrough stream when archiver finalize rejects with a non-Error value', async () => {
// Covers the `err instanceof Error ? err : new Error(String(err))` false branch
mockFinalize.mockReturnValue(Promise.reject('string-error'));
const { stream } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'typescript',
});
// Attach error listener to suppress unhandled error event from stream.destroy(err)
await new Promise<void>((resolve) => {
stream.on('error', () => resolve());
});
expect((stream as PassThrough).destroyed).toBe(true);
});
});

View File

@@ -0,0 +1,128 @@
/**
* Unit tests for src/services/ScaffoldService.ts
*/
import { ScaffoldService } from '../../../src/services/ScaffoldService';
import { ValidationError } from '../../../src/utils/errors';
describe('ScaffoldService', () => {
let service: ScaffoldService;
beforeEach(() => {
service = new ScaffoldService();
});
const BASE_OPTIONS = {
agentId: 'agent-uuid-1234',
agentName: 'my-test-agent',
clientId: 'client-id-5678',
apiUrl: 'https://api.sentryagent.ai',
};
// Helper to collect stream into a Buffer
async function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk: Buffer) => chunks.push(chunk));
stream.on('end', () => resolve(Buffer.concat(chunks)));
stream.on('error', reject);
});
}
describe('generateScaffold', () => {
it('generates a TypeScript scaffold ZIP with all 6 template files', async () => {
const { stream, filename } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'typescript',
});
expect(filename).toMatch(/sentryagent-scaffold.*typescript\.zip$/);
// Verify stream produces a non-empty buffer (valid ZIP magic bytes: PK)
const buf = await streamToBuffer(stream);
expect(buf.length).toBeGreaterThan(0);
expect(buf[0]).toBe(0x50); // 'P'
expect(buf[1]).toBe(0x4b); // 'K'
});
it('generates a Python scaffold ZIP with all 5 template files', async () => {
const { stream, filename } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'python',
});
expect(filename).toMatch(/sentryagent-scaffold.*python\.zip$/);
const buf = await streamToBuffer(stream);
expect(buf.length).toBeGreaterThan(0);
});
it('generates a Go scaffold ZIP', async () => {
const { stream, filename } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'go',
});
expect(filename).toMatch(/go\.zip$/);
const buf = await streamToBuffer(stream);
expect(buf.length).toBeGreaterThan(0);
});
it('generates a Java scaffold ZIP', async () => {
const { stream } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'java',
});
const buf = await streamToBuffer(stream);
expect(buf.length).toBeGreaterThan(0);
});
it('generates a Rust scaffold ZIP', async () => {
const { stream } = await service.generateScaffold({
...BASE_OPTIONS,
language: 'rust',
});
const buf = await streamToBuffer(stream);
expect(buf.length).toBeGreaterThan(0);
});
it('injects {{CLIENT_ID}} into the template variables — no {{CLIENT_ID}} placeholder in output', () => {
// Verify the service's injectVariables method replaces all placeholders.
// We do this by checking the .env.example template source — it uses {{CLIENT_ID}},
// and the service must replace it. We verify the template has the placeholder.
const { readFileSync } = require('fs');
const { join } = require('path');
const templatePath = join(
__dirname,
'../../../src/templates/scaffold/typescript/.env.example.tmpl',
);
const template = readFileSync(templatePath, 'utf-8');
expect(template).toContain('{{CLIENT_ID}}');
// Verify injection: replace manually and check
const injected = template.replace(/\{\{CLIENT_ID\}\}/g, 'injected-client-id-xyz');
expect(injected).toContain('injected-client-id-xyz');
expect(injected).not.toContain('{{CLIENT_ID}}');
});
it('never injects real client secret — .env.example template has placeholder only', () => {
// Read the source template directly — it must contain the placeholder, never a real secret
const { readFileSync } = require('fs');
const { join } = require('path');
const templatePath = join(
__dirname,
'../../../src/templates/scaffold/typescript/.env.example.tmpl',
);
const template = readFileSync(templatePath, 'utf-8');
expect(template).toContain('<your-client-secret>');
// The template must NOT contain {{CLIENT_SECRET}} or any other secret injection point
expect(template).not.toContain('{{CLIENT_SECRET}}');
});
it('throws ValidationError for an unsupported language', async () => {
await expect(
service.generateScaffold({
...BASE_OPTIONS,
language: 'cobol' as never,
}),
).rejects.toThrow(ValidationError);
});
});
});