feat: add universal resource activation policy profile flow
This commit is contained in:
@@ -204,7 +204,7 @@ CT 2301 (besu-rpc-private-1) may fail to start with `lxc.hook.pre-start` due to
|
||||
|
||||
- **Daily/weekly checks:** `./scripts/maintenance/daily-weekly-checks.sh [daily|weekly|all]` — explorer sync (135), RPC health (136), config API (137). **Cron:** `./scripts/maintenance/schedule-daily-weekly-cron.sh [--install|--show]` (daily 08:00, weekly Sun 09:00). See [OPERATIONAL_RUNBOOKS.md](../docs/03-deployment/OPERATIONAL_RUNBOOKS.md) § Maintenance.
|
||||
- **Start firefly-ali-1 (6201):** `./scripts/maintenance/start-firefly-6201.sh [--dry-run] [--host HOST]` — start CT 6201 on r630-02 when needed (optional ongoing).
|
||||
- **Config validation (pre-deploy):** `./scripts/validation/validate-config-files.sh` — set `VALIDATE_REQUIRED_FILES` for required paths. **CI / all validation:** `./scripts/verify/run-all-validation.sh [--skip-genesis] [--json-out reports/status/run-all-validation-latest.json]` — dependencies, config files, **cW\* mesh matrix** (merge of `cross-chain-pmm-lps/config/deployment-status.json` and `reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json` when that file exists; no RPC), optional genesis (no LAN/SSH). **Matrix only:** `./scripts/verify/build-cw-mesh-deployment-matrix.sh` — stdout markdown; `--json-out reports/status/cw-mesh-deployment-matrix-latest.json` for machine-readable rows. **URA (universal resource activation) smoke:** `./scripts/verify/smoke-universal-resource-activation.sh` (JSON Schema only) or the same with `--http` and optional `PHOENIX_BASE_URL` to assert Phoenix `GET /api/v1/universal-resource-activation/manifest`; see `docs/04-configuration/universal-resource-activation/UNIVERSAL_RESOURCE_WIRING.md` §2.1.
|
||||
- **Config validation (pre-deploy):** `./scripts/validation/validate-config-files.sh` — set `VALIDATE_REQUIRED_FILES` for required paths. **CI / all validation:** `./scripts/verify/run-all-validation.sh [--skip-genesis] [--json-out reports/status/run-all-validation-latest.json]` — dependencies, config files, **cW\* mesh matrix** (merge of `cross-chain-pmm-lps/config/deployment-status.json` and `reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json` when that file exists; no RPC), optional genesis (no LAN/SSH). **Matrix only:** `./scripts/verify/build-cw-mesh-deployment-matrix.sh` — stdout markdown; `--json-out reports/status/cw-mesh-deployment-matrix-latest.json` for machine-readable rows. **URA (universal resource activation):** **`pnpm ura:validate`**, **`pnpm ura:validate-profiles`**, **`pnpm ura:merge-manifest`**, **`pnpm ura:validate-ledger-mapping`**, **`pnpm ura:writer:ledger`**, **`pnpm ura:writer:settlement`**, **`pnpm ura:profile-hash`**, **`pnpm ura:validate-closure`** / **`pnpm ura:validate-closure:strict`**, **`pnpm ura:keccak`**, **`pnpm ura:smoke`**. Optional **`URA_STRICT_CLOSURE=1`**. Tracker: `docs/04-configuration/universal-resource-activation/URA_MANIFEST_AUTOMATION_IMPLEMENTATION_TRACKER.md`. See `UNIVERSAL_RESOURCE_WIRING.md` §2.1 and §5; multi-jurisdiction: `docs/04-configuration/compliance-matrices/README.md`.
|
||||
- **Wrapper summaries:** `./scripts/run-completable-tasks-from-anywhere.sh --json-out reports/status/run-completable-tasks-latest.json`, `./scripts/run-e2e-flow-tasks-full-parallel.sh --json-out reports/status/run-e2e-flow-tasks-latest.json`, `./scripts/deployment/run-all-next-steps-chain138.sh --json-out reports/status/run-all-next-steps-chain138-latest.json`, and `./scripts/run-all-operator-tasks-from-lan.sh --json-out reports/status/run-all-operator-tasks-latest.json` produce machine-readable step summaries that match the terminal progress output.
|
||||
|
||||
### 13. Phase 2, 3 & 4 Deployment Scripts
|
||||
|
||||
38
scripts/ura/keccak-resource-ids.mjs
Executable file
38
scripts/ura/keccak-resource-ids.mjs
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Emit keccak256(utf8(resourceId)) for each row in the URA manifest.
|
||||
* Use when anchoring resourceId in on-chain / GRU registries (operator step; not automatic mirroring).
|
||||
*
|
||||
* Usage: from repo root — node scripts/ura/keccak-resource-ids.mjs
|
||||
* Requires: root devDependency `ethers` (v6).
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { keccak256, toUtf8Bytes } from 'ethers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.join(__dirname, '../..');
|
||||
const manifestPath = path.join(projectRoot, 'config', 'universal-resource-activation', 'manifest.json');
|
||||
|
||||
if (!existsSync(manifestPath)) {
|
||||
console.error(`[keccak-ura] Missing ${manifestPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
const resources = m.resources || [];
|
||||
|
||||
console.log(
|
||||
'# keccak256(utf8(resourceId)) for universal-resource-activation resources\n# Use for optional on-chain / GRU registry rows; keep manifest as canonical off-chain store.\n',
|
||||
);
|
||||
|
||||
for (const r of resources) {
|
||||
const id = r.resourceId;
|
||||
if (typeof id !== 'string' || !id) continue;
|
||||
const h = keccak256(toUtf8Bytes(id));
|
||||
console.log(id);
|
||||
console.log(` ${h}`);
|
||||
console.log('');
|
||||
}
|
||||
console.log(`[keccak-ura] ${resources.length} resource(s)`);
|
||||
134
scripts/ura/lib/validate-ura-manifest.mjs
Normal file
134
scripts/ura/lib/validate-ura-manifest.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Shared URA manifest validation (schemas + cross-checks).
|
||||
* Used by validate-universal-resource-activation.mjs and merge-manifest-fragments.mjs.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function getProjectRoot() {
|
||||
return path.resolve(path.join(__dirname, '..', '..', '..'));
|
||||
}
|
||||
|
||||
export function getDefaultManifestPath(projectRoot = getProjectRoot()) {
|
||||
return path.join(projectRoot, 'config', 'universal-resource-activation', 'manifest.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ validateManifest: import('ajv').ValidateFunction, validateResource: import('ajv').ValidateFunction, validateEvidence: import('ajv').ValidateFunction, ajv: import('ajv').default }}
|
||||
*/
|
||||
export function loadUraManifestValidators(projectRoot = getProjectRoot()) {
|
||||
const manifestSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.manifest.v1.schema.json');
|
||||
const resourceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.resource.v1.schema.json');
|
||||
const evidenceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.evidence-package.v1.schema.json');
|
||||
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
strict: false,
|
||||
validateSchema: false,
|
||||
});
|
||||
addFormats(ajv);
|
||||
|
||||
const manifestSchema = JSON.parse(readFileSync(manifestSchemaPath, 'utf8'));
|
||||
const resourceSchema = JSON.parse(readFileSync(resourceSchemaPath, 'utf8'));
|
||||
const evidenceSchema = JSON.parse(readFileSync(evidenceSchemaPath, 'utf8'));
|
||||
|
||||
return {
|
||||
validateManifest: ajv.compile(manifestSchema),
|
||||
validateResource: ajv.compile(resourceSchema),
|
||||
validateEvidence: ajv.compile(evidenceSchema),
|
||||
ajv,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} data
|
||||
* @param {{ validateManifest: import('ajv').ValidateFunction, validateResource: import('ajv').ValidateFunction, validateEvidence: import('ajv').ValidateFunction }} validators
|
||||
* @returns {string[]} empty if valid
|
||||
*/
|
||||
export function validateUraManifestData(data, validators) {
|
||||
const errors = [];
|
||||
const { validateManifest, validateResource, validateEvidence, ajv } = validators;
|
||||
|
||||
if (!validateManifest(data)) {
|
||||
return [`Manifest failed manifest schema: ${ajv.errorsText(validateManifest.errors, { separator: '\n' })}`];
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.resources)) {
|
||||
return ['resources must be an array'];
|
||||
}
|
||||
if (!Array.isArray(data.evidencePackages)) {
|
||||
return ['evidencePackages must be an array'];
|
||||
}
|
||||
|
||||
data.resources.forEach((r, i) => {
|
||||
if (!validateResource(r)) {
|
||||
errors.push(
|
||||
`resources[${i}] (resourceId=${r?.resourceId}): ${ajv.errorsText(validateResource.errors, { separator: '\n' })}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
data.evidencePackages.forEach((p, i) => {
|
||||
if (!validateEvidence(p)) {
|
||||
errors.push(
|
||||
`evidencePackages[${i}] (id=${p?.evidencePackageId}): ${ajv.errorsText(validateEvidence.errors, { separator: '\n' })}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) return errors;
|
||||
|
||||
const ids = new Set(data.resources.map((r) => r.resourceId).filter(Boolean));
|
||||
data.evidencePackages.forEach((p, pi) => {
|
||||
(p.resourceIds || []).forEach((rid) => {
|
||||
if (!ids.has(rid)) {
|
||||
errors.push(`evidencePackages[${pi}] references unknown resourceId: ${rid}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-style: read file, validate, log, exit 0 or 1.
|
||||
* @param {string} [manifestPath]
|
||||
*/
|
||||
export function validateUraManifestFileCli(manifestPath) {
|
||||
const projectRoot = getProjectRoot();
|
||||
const pathToUse = manifestPath || getDefaultManifestPath(projectRoot);
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`[validate-ura] ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(pathToUse)) {
|
||||
fail(`Missing ${pathToUse}`);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(readFileSync(pathToUse, 'utf8'));
|
||||
} catch (e) {
|
||||
fail(`Invalid JSON: ${e.message}`);
|
||||
}
|
||||
|
||||
const validators = loadUraManifestValidators(projectRoot);
|
||||
const errs = validateUraManifestData(data, validators);
|
||||
if (errs.length) {
|
||||
errs.forEach((e) => console.error(`[validate-ura] ${e}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[validate-ura] OK: ${data.resources.length} resource(s), ${data.evidencePackages.length} evidence package(s)`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
36
scripts/ura/manifest-writer/README.md
Normal file
36
scripts/ura/manifest-writer/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# URA manifest writer (ledger + settlement fragments)
|
||||
|
||||
**Purpose:** Build **partial manifest fragments** from machine-readable inputs so ops or a batch job can merge them (`pnpm ura:merge-manifest` or manual paste) after validation.
|
||||
|
||||
## Ledger → `accountingRef`
|
||||
|
||||
1. Define mapping: [`omnl-ledger-mapping.v1.example.json`](../../../config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json) → copy to `omnl-ledger-mapping.v1.json` (gitignored or secured).
|
||||
2. Export ledger snapshot JSON (Fineract/OMNL/sidecar ETL).
|
||||
3. Run:
|
||||
|
||||
```bash
|
||||
pnpm ura:writer:ledger -- \
|
||||
--mapping config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json \
|
||||
--ledger config/universal-resource-activation/integration/examples/ledger-snapshot.example.json
|
||||
```
|
||||
|
||||
4. Pipe output into a file under `manifest-fragments/` or merge with `merge-manifest-fragments.mjs --out`.
|
||||
|
||||
Validate mapping: `pnpm ura:validate-ledger-mapping -- config/.../omnl-ledger-mapping.v1.example.json`
|
||||
|
||||
## Rail / chain → `settlementOrChainRef`
|
||||
|
||||
```bash
|
||||
pnpm ura:writer:settlement -- \
|
||||
--evidence-package-id ura:pilot:evidence-register-bootstrap \
|
||||
--message-id 0xabc \
|
||||
--tx-hash 0xdef \
|
||||
--chain-id 138
|
||||
```
|
||||
|
||||
Emits a fragment with a single `evidencePackages[]` row (shallow merge by id).
|
||||
|
||||
## Related
|
||||
|
||||
- [`URA_MANIFEST_WRITER_OPS.md`](../../../docs/03-deployment/URA_MANIFEST_WRITER_OPS.md)
|
||||
- [`URA_MANIFEST_AUTOMATION_IMPLEMENTATION_TRACKER.md`](../../../docs/04-configuration/universal-resource-activation/URA_MANIFEST_AUTOMATION_IMPLEMENTATION_TRACKER.md)
|
||||
71
scripts/ura/manifest-writer/build-ledger-fragment.mjs
Normal file
71
scripts/ura/manifest-writer/build-ledger-fragment.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build a manifest fragment from a ledger snapshot JSON + omnl-ledger-mapping.v1.json
|
||||
* See scripts/ura/manifest-writer/README.md
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getByPath, asString } from './lib/get-by-path.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.join(__dirname, '..', '..', '..');
|
||||
|
||||
function parseArgs() {
|
||||
const a = process.argv.slice(2);
|
||||
const o = {};
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] === '--mapping') o.mapping = a[++i];
|
||||
else if (a[i] === '--ledger') o.ledger = a[++i];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
const args = parseArgs();
|
||||
const mappingPath = path.resolve(projectRoot, args.mapping || 'config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json');
|
||||
const ledgerPath = path.resolve(
|
||||
projectRoot,
|
||||
args.ledger || 'config/universal-resource-activation/integration/examples/ledger-snapshot.example.json'
|
||||
);
|
||||
|
||||
for (const p of [mappingPath, ledgerPath]) {
|
||||
if (!existsSync(p)) {
|
||||
console.error(`[writer-ledger] Missing file: ${p}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let mapping;
|
||||
let ledger;
|
||||
try {
|
||||
mapping = JSON.parse(readFileSync(mappingPath, 'utf8'));
|
||||
ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(`[writer-ledger] JSON error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const evidencePackages = [];
|
||||
for (const row of mapping.evidencePackages || []) {
|
||||
const pkg = { evidencePackageId: row.evidencePackageId };
|
||||
if (row.accountingRefField) {
|
||||
const v = getByPath(ledger, row.accountingRefField);
|
||||
pkg.accountingRef = asString(v);
|
||||
}
|
||||
evidencePackages.push(pkg);
|
||||
}
|
||||
|
||||
const resources = [];
|
||||
for (const ru of mapping.resourceUpdates || []) {
|
||||
const v = getByPath(ledger, ru.quantityField);
|
||||
resources.push({
|
||||
resourceId: ru.resourceId,
|
||||
quantity: asString(v),
|
||||
});
|
||||
}
|
||||
|
||||
const fragment = {};
|
||||
if (evidencePackages.length) fragment.evidencePackages = evidencePackages;
|
||||
if (resources.length) fragment.resources = resources;
|
||||
|
||||
process.stdout.write(`${JSON.stringify(fragment, null, 2)}\n`);
|
||||
38
scripts/ura/manifest-writer/build-settlement-fragment.mjs
Normal file
38
scripts/ura/manifest-writer/build-settlement-fragment.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build a manifest fragment with settlementOrChainRef on an evidence package.
|
||||
*/
|
||||
function parseArgs() {
|
||||
const a = process.argv.slice(2);
|
||||
const o = {};
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] === '--evidence-package-id') o.evidencePackageId = a[++i];
|
||||
else if (a[i] === '--message-id') o.messageId = a[++i];
|
||||
else if (a[i] === '--tx-hash') o.txHash = a[++i];
|
||||
else if (a[i] === '--chain-id') o.chainId = a[++i];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
const args = parseArgs();
|
||||
if (!args.evidencePackageId) {
|
||||
console.error('[writer-settlement] Required: --evidence-package-id');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (args.messageId) parts.push(`messageId=${args.messageId}`);
|
||||
if (args.txHash) parts.push(`tx=${args.txHash}`);
|
||||
if (args.chainId) parts.push(`chain=${args.chainId}`);
|
||||
const settlementOrChainRef = parts.length ? parts.join(';') : '';
|
||||
|
||||
const fragment = {
|
||||
evidencePackages: [
|
||||
{
|
||||
evidencePackageId: args.evidencePackageId,
|
||||
settlementOrChainRef,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
process.stdout.write(`${JSON.stringify(fragment, null, 2)}\n`);
|
||||
25
scripts/ura/manifest-writer/lib/get-by-path.mjs
Normal file
25
scripts/ura/manifest-writer/lib/get-by-path.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @param {Record<string, unknown>} obj
|
||||
* @param {string} dotPath e.g. "a.b.c"
|
||||
* @returns {unknown}
|
||||
*/
|
||||
export function getByPath(obj, dotPath) {
|
||||
if (!dotPath || typeof dotPath !== 'string') return undefined;
|
||||
const parts = dotPath.split('.').filter(Boolean);
|
||||
let cur = obj;
|
||||
for (const p of parts) {
|
||||
if (cur == null || typeof cur !== 'object') return undefined;
|
||||
cur = /** @type {Record<string, unknown>} */ (cur)[p];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} v
|
||||
* @returns {string}
|
||||
*/
|
||||
export function asString(v) {
|
||||
if (v == null) return '';
|
||||
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
162
scripts/ura/merge-manifest-fragments.mjs
Normal file
162
scripts/ura/merge-manifest-fragments.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Merge optional JSON fragments into the URA manifest and validate (dry-run or --out).
|
||||
*
|
||||
* Fragments: partial objects with optional keys resources[], evidencePackages[], policyProfileRefs[].
|
||||
* Later files in sort order override same resourceId / evidencePackageId.
|
||||
*
|
||||
* Usage (repo root):
|
||||
* node scripts/ura/merge-manifest-fragments.mjs
|
||||
* node scripts/ura/merge-manifest-fragments.mjs --out /tmp/merged-manifest.json
|
||||
* node scripts/ura/merge-manifest-fragments.mjs --base config/universal-resource-activation/manifest.json --fragments-dir config/universal-resource-activation/manifest-fragments
|
||||
*/
|
||||
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
getProjectRoot,
|
||||
getDefaultManifestPath,
|
||||
loadUraManifestValidators,
|
||||
validateUraManifestData,
|
||||
} from './lib/validate-ura-manifest.mjs';
|
||||
|
||||
function parseArgs() {
|
||||
const argv = process.argv.slice(2);
|
||||
const out = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--out') out.out = argv[++i];
|
||||
else if (a === '--base') out.base = argv[++i];
|
||||
else if (a === '--fragments-dir') out.fragmentsDir = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function deepClone(o) {
|
||||
return JSON.parse(JSON.stringify(o));
|
||||
}
|
||||
|
||||
function profileKey(ref) {
|
||||
if (!ref || typeof ref.id !== 'string') return null;
|
||||
const v = ref.version != null ? String(ref.version) : '';
|
||||
return `${ref.id}@${v}`;
|
||||
}
|
||||
|
||||
function mergeFragment(base, frag, fragmentLabel) {
|
||||
const issues = [];
|
||||
|
||||
if (frag.policyProfileRefs && Array.isArray(frag.policyProfileRefs)) {
|
||||
const map = new Map();
|
||||
(base.policyProfileRefs || []).forEach((r) => {
|
||||
const k = profileKey(r);
|
||||
if (k) map.set(k, r);
|
||||
});
|
||||
frag.policyProfileRefs.forEach((r) => {
|
||||
const k = profileKey(r);
|
||||
if (k) map.set(k, r);
|
||||
});
|
||||
base.policyProfileRefs = [...map.values()].sort((a, b) => {
|
||||
const ka = profileKey(a);
|
||||
const kb = profileKey(b);
|
||||
return ka.localeCompare(kb);
|
||||
});
|
||||
}
|
||||
|
||||
if (frag.resources && Array.isArray(frag.resources)) {
|
||||
const byId = new Map((base.resources || []).map((r) => [r.resourceId, r]));
|
||||
frag.resources.forEach((r) => {
|
||||
if (!r || typeof r.resourceId !== 'string') {
|
||||
issues.push(`${fragmentLabel}: skip resource without resourceId`);
|
||||
return;
|
||||
}
|
||||
issues.push(
|
||||
`${fragmentLabel}: ${byId.has(r.resourceId) ? 'replace' : 'add'} resource ${r.resourceId}`
|
||||
);
|
||||
byId.set(r.resourceId, { ...(byId.get(r.resourceId) || {}), ...r });
|
||||
});
|
||||
base.resources = [...byId.values()].sort((a, b) => String(a.resourceId).localeCompare(String(b.resourceId)));
|
||||
}
|
||||
|
||||
if (frag.evidencePackages && Array.isArray(frag.evidencePackages)) {
|
||||
const byId = new Map((base.evidencePackages || []).map((p) => [p.evidencePackageId, p]));
|
||||
frag.evidencePackages.forEach((p) => {
|
||||
if (!p || typeof p.evidencePackageId !== 'string') {
|
||||
issues.push(`${fragmentLabel}: skip evidence package without evidencePackageId`);
|
||||
return;
|
||||
}
|
||||
issues.push(
|
||||
`${fragmentLabel}: ${byId.has(p.evidencePackageId) ? 'replace' : 'add'} evidencePackage ${p.evidencePackageId}`
|
||||
);
|
||||
const prev = byId.get(p.evidencePackageId) || {};
|
||||
byId.set(p.evidencePackageId, { ...prev, ...p });
|
||||
});
|
||||
base.evidencePackages = [...byId.values()].sort((a, b) =>
|
||||
String(a.evidencePackageId).localeCompare(String(b.evidencePackageId))
|
||||
);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs();
|
||||
const projectRoot = getProjectRoot();
|
||||
const basePath = path.resolve(projectRoot, args.base || getDefaultManifestPath(projectRoot));
|
||||
const fragmentsDir = path.resolve(
|
||||
projectRoot,
|
||||
args.fragmentsDir || path.join('config', 'universal-resource-activation', 'manifest-fragments')
|
||||
);
|
||||
|
||||
if (!existsSync(basePath)) {
|
||||
console.error(`[merge-ura-manifest] Missing base: ${basePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let merged;
|
||||
try {
|
||||
merged = JSON.parse(readFileSync(basePath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(`[merge-ura-manifest] Invalid base JSON: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mergeNotes = [];
|
||||
if (existsSync(fragmentsDir)) {
|
||||
const files = readdirSync(fragmentsDir)
|
||||
.filter((f) => f.endsWith('.json') && !f.startsWith('_'))
|
||||
.sort();
|
||||
for (const f of files) {
|
||||
const fp = path.join(fragmentsDir, f);
|
||||
let frag;
|
||||
try {
|
||||
frag = JSON.parse(readFileSync(fp, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(`[merge-ura-manifest] Invalid JSON in ${fp}: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
mergeNotes.push(...mergeFragment(merged, frag, f));
|
||||
}
|
||||
}
|
||||
|
||||
const validators = loadUraManifestValidators(projectRoot);
|
||||
const errs = validateUraManifestData(merged, validators);
|
||||
if (errs.length) {
|
||||
errs.forEach((e) => console.error(`[merge-ura-manifest] ${e}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mergeNotes.forEach((n) => console.log(`[merge-ura-manifest] ${n}`));
|
||||
console.log(
|
||||
`[merge-ura-manifest] OK: ${merged.resources.length} resource(s), ${merged.evidencePackages.length} evidence package(s)`
|
||||
);
|
||||
|
||||
if (args.out) {
|
||||
writeFileSync(path.resolve(args.out), `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
|
||||
console.log(`[merge-ura-manifest] Wrote ${path.resolve(args.out)}`);
|
||||
} else {
|
||||
console.log('[merge-ura-manifest] Dry-run (pass --out <file> to write merged JSON)');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
46
scripts/ura/policy-profiles-content-hash.mjs
Normal file
46
scripts/ura/policy-profiles-content-hash.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Compute a content hash for one policy profile row (for PolicyProfileRegistry.publishProfile on-chain).
|
||||
* Uses keccak256(utf8(canonicalJson)) where canonicalJson is stable key-sorted JSON of the profile object.
|
||||
*
|
||||
* Usage: node scripts/ura/policy-profiles-content-hash.mjs <policyProfileId>
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { keccak256, toUtf8Bytes } from 'ethers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.join(__dirname, '..', '..');
|
||||
const registryPath = path.join(projectRoot, 'config/universal-resource-activation/policy-profiles.json');
|
||||
|
||||
const id = process.argv[2];
|
||||
if (!id) {
|
||||
console.error('Usage: node scripts/ura/policy-profiles-content-hash.mjs <policyProfileId>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(registryPath)) {
|
||||
console.error(`Missing ${registryPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = JSON.parse(readFileSync(registryPath, 'utf8'));
|
||||
const profile = (data.profiles || []).find((p) => p.policyProfileId === id);
|
||||
if (!profile) {
|
||||
console.error(`Unknown policyProfileId: ${id}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function sortKeys(obj) {
|
||||
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return obj;
|
||||
const out = {};
|
||||
for (const k of Object.keys(obj).sort()) {
|
||||
out[k] = sortKeys(obj[k]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const canonical = JSON.stringify(sortKeys(profile));
|
||||
const hash = keccak256(toUtf8Bytes(canonical));
|
||||
console.log(JSON.stringify({ policyProfileId: id, contentHash: hash, canonicalBytes: canonical.length }, null, 2));
|
||||
81
scripts/ura/validate-manifest-closure.mjs
Normal file
81
scripts/ura/validate-manifest-closure.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Optional production gate: fail if manifest still contains pilot placeholders or open reconciliation.
|
||||
*
|
||||
* Usage (repo root):
|
||||
* node scripts/ura/validate-manifest-closure.mjs # warnings only, exit 0
|
||||
* node scripts/ura/validate-manifest-closure.mjs --strict # exit 1 on violations
|
||||
*
|
||||
* CI: set URA_STRICT_CLOSURE=1 and run validate-config-files.sh (see script header there).
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { getProjectRoot, getDefaultManifestPath } from './lib/validate-ura-manifest.mjs';
|
||||
|
||||
const PILOT_PARTICIPANT = /ura:participant:pilot-/i;
|
||||
const PENDING_EVIDENCE = /^ura:evidence:pending-/i;
|
||||
const TBD_RE = /\bTBD\b/i;
|
||||
|
||||
const strict = process.argv.includes('--strict');
|
||||
const projectRoot = getProjectRoot();
|
||||
const manifestPath = getDefaultManifestPath(projectRoot);
|
||||
|
||||
function main() {
|
||||
const log = (m) => console.log(`[validate-ura-closure] ${m}`);
|
||||
const warn = (m) => console.warn(`[validate-ura-closure] WARN: ${m}`);
|
||||
const err = (m) => console.error(`[validate-ura-closure] ${strict ? 'FAIL' : 'WARN'}: ${m}`);
|
||||
|
||||
if (!existsSync(manifestPath)) {
|
||||
console.error(`[validate-ura-closure] Missing ${manifestPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(`[validate-ura-closure] Invalid JSON: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
|
||||
(data.resources || []).forEach((r, i) => {
|
||||
const pid = r.ownerParticipantId;
|
||||
if (typeof pid === 'string' && PILOT_PARTICIPANT.test(pid)) {
|
||||
violations.push(`resources[${i}] ownerParticipantId is pilot placeholder: ${pid}`);
|
||||
}
|
||||
(r.evidenceRefs || []).forEach((ref, j) => {
|
||||
if (typeof ref === 'string' && PENDING_EVIDENCE.test(ref)) {
|
||||
violations.push(`resources[${i}] evidenceRefs[${j}] pending: ${ref}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
(data.evidencePackages || []).forEach((p, i) => {
|
||||
if (p.reconciliationStatus === 'open') {
|
||||
violations.push(`evidencePackages[${i}] (${p.evidencePackageId}) reconciliationStatus is open`);
|
||||
}
|
||||
const checkFields = ['custodyOrSourceEvidence', 'accountingRef', 'settlementOrChainRef', 'deploymentRef', 'explanation'];
|
||||
for (const f of checkFields) {
|
||||
const v = p[f];
|
||||
if (typeof v === 'string' && TBD_RE.test(v)) {
|
||||
violations.push(`evidencePackages[${i}] (${p.evidencePackageId}).${f} contains TBD`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (violations.length === 0) {
|
||||
log('OK: no pilot/TBD/open-reconciliation patterns detected');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
violations.forEach((v) => (strict ? err : warn)(v));
|
||||
if (strict) {
|
||||
err(`${violations.length} violation(s) — close pilots per URA_PILOT_CLOSURE_RUNBOOK.md`);
|
||||
process.exit(1);
|
||||
}
|
||||
log(`${violations.length} notice(s) (use --strict to fail CI)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
51
scripts/validate/validate-omnl-ledger-mapping.mjs
Normal file
51
scripts/validate/validate-omnl-ledger-mapping.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate omnl-ledger-mapping.v1.json against omnl-ledger-mapping.v1.schema.json
|
||||
* Usage: node scripts/validate/validate-omnl-ledger-mapping.mjs [path]
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.join(__dirname, '../..');
|
||||
const defaultPath = path.join(
|
||||
projectRoot,
|
||||
'config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json'
|
||||
);
|
||||
const schemaPath = path.join(
|
||||
projectRoot,
|
||||
'config/universal-resource-activation/integration/omnl-ledger-mapping.v1.schema.json'
|
||||
);
|
||||
|
||||
const file = path.resolve(projectRoot, process.argv[2] || defaultPath);
|
||||
|
||||
if (!existsSync(file)) {
|
||||
console.error(`[validate-ledger-mapping] Missing ${file}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!existsSync(schemaPath)) {
|
||||
console.error(`[validate-ledger-mapping] Missing schema ${schemaPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, strict: false, validateSchema: false });
|
||||
addFormats(ajv);
|
||||
const validate = ajv.compile(JSON.parse(readFileSync(schemaPath, 'utf8')));
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(readFileSync(file, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(`[validate-ledger-mapping] Invalid JSON: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!validate(data)) {
|
||||
console.error(`[validate-ledger-mapping] FAIL: ${ajv.errorsText(validate.errors, { separator: '\n' })}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[validate-ledger-mapping] OK: ${file}`);
|
||||
process.exit(0);
|
||||
@@ -1,100 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate config/universal-resource-activation/manifest.json against
|
||||
* - universal-resource-activation.manifest.v1.schema.json
|
||||
* - universal-resource-activation.resource.v1.schema.json (per item in resources[])
|
||||
* - universal-resource-activation.evidence-package.v1.schema.json (per item in evidencePackages[])
|
||||
* Validate config/universal-resource-activation/manifest.json against URA JSON Schemas.
|
||||
*
|
||||
* Usage: from repo root: node scripts/validate/validate-universal-resource-activation.mjs
|
||||
* Exit 0 on success, 1 on error.
|
||||
* Usage: from repo root: node scripts/validate/validate-universal-resource-activation.mjs [path/to/manifest.json]
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import { validateUraManifestFileCli } from '../ura/lib/validate-ura-manifest.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.join(__dirname, '../..');
|
||||
const configDir = path.join(projectRoot, 'config', 'universal-resource-activation');
|
||||
|
||||
const manifestPath = path.join(configDir, 'manifest.json');
|
||||
const manifestSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.manifest.v1.schema.json');
|
||||
const resourceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.resource.v1.schema.json');
|
||||
const evidenceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.evidence-package.v1.schema.json');
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`[validate-ura] ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(manifestPath)) {
|
||||
fail(`Missing ${manifestPath}`);
|
||||
}
|
||||
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
strict: false,
|
||||
validateSchema: false,
|
||||
});
|
||||
addFormats(ajv);
|
||||
|
||||
const manifestSchema = JSON.parse(readFileSync(manifestSchemaPath, 'utf8'));
|
||||
const resourceSchema = JSON.parse(readFileSync(resourceSchemaPath, 'utf8'));
|
||||
const evidenceSchema = JSON.parse(readFileSync(evidenceSchemaPath, 'utf8'));
|
||||
|
||||
const validateManifest = ajv.compile(manifestSchema);
|
||||
const validateResource = ajv.compile(resourceSchema);
|
||||
const validateEvidence = ajv.compile(evidenceSchema);
|
||||
|
||||
const raw = readFileSync(manifestPath, 'utf8');
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
fail(`Invalid JSON: ${e.message}`);
|
||||
}
|
||||
|
||||
if (!validateManifest(data)) {
|
||||
fail(`Manifest failed manifest schema: ${ajv.errorsText(validateManifest.errors, { separator: '\n' })}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.resources)) {
|
||||
fail('resources must be an array');
|
||||
}
|
||||
if (!Array.isArray(data.evidencePackages)) {
|
||||
fail('evidencePackages must be an array');
|
||||
}
|
||||
|
||||
data.resources.forEach((r, i) => {
|
||||
if (!validateResource(r)) {
|
||||
fail(
|
||||
`resources[${i}] (resourceId=${r?.resourceId}): ${ajv.errorsText(validateResource.errors, { separator: '\n' })}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
data.evidencePackages.forEach((p, i) => {
|
||||
if (!validateEvidence(p)) {
|
||||
fail(
|
||||
`evidencePackages[${i}] (id=${p?.evidencePackageId}): ${ajv.errorsText(validateEvidence.errors, { separator: '\n' })}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cross-check: all resourceIds referenced in evidence exist
|
||||
const ids = new Set(data.resources.map((r) => r.resourceId).filter(Boolean));
|
||||
data.evidencePackages.forEach((p, pi) => {
|
||||
(p.resourceIds || []).forEach((rid) => {
|
||||
if (!ids.has(rid)) {
|
||||
fail(
|
||||
`evidencePackages[${pi}] references unknown resourceId: ${rid}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[validate-ura] OK: ${data.resources.length} resource(s), ${data.evidencePackages.length} evidence package(s)`
|
||||
);
|
||||
process.exit(0);
|
||||
const arg = process.argv[2];
|
||||
validateUraManifestFileCli(arg || undefined);
|
||||
|
||||
65
scripts/validate/validate-ura-policy-profiles.mjs
Normal file
65
scripts/validate/validate-ura-policy-profiles.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate config/universal-resource-activation/policy-profiles.json against
|
||||
* universal-resource-activation.policy-profile-registry.v1.schema.json
|
||||
* and ensure manifest policyProfileRefs[] ids exist in the registry.
|
||||
*
|
||||
* Usage: from repo root — node scripts/validate/validate-ura-policy-profiles.mjs
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.join(__dirname, '../..');
|
||||
|
||||
const registryPath = path.join(projectRoot, 'config', 'universal-resource-activation', 'policy-profiles.json');
|
||||
const registrySchemaPath = path.join(
|
||||
projectRoot,
|
||||
'config',
|
||||
'universal-resource-activation.policy-profile-registry.v1.schema.json',
|
||||
);
|
||||
const manifestPath = path.join(projectRoot, 'config', 'universal-resource-activation', 'manifest.json');
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`[validate-ura-profiles] ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(registryPath)) fail(`Missing ${registryPath}`);
|
||||
if (!existsSync(registrySchemaPath)) fail(`Missing ${registrySchemaPath}`);
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, strict: false, validateSchema: false });
|
||||
addFormats(ajv);
|
||||
|
||||
const registrySchema = JSON.parse(readFileSync(registrySchemaPath, 'utf8'));
|
||||
const validateRegistry = ajv.compile(registrySchema);
|
||||
const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
||||
|
||||
if (!validateRegistry(registry)) {
|
||||
console.error('[validate-ura-profiles] policy-profiles.json failed schema:', validateRegistry.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ids = new Set(registry.profiles.map((p) => p.policyProfileId));
|
||||
console.log(`[validate-ura-profiles] OK: ${ids.size} profile(s) in registry`);
|
||||
|
||||
if (existsSync(manifestPath)) {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
const refs = manifest.policyProfileRefs || [];
|
||||
for (const r of refs) {
|
||||
const pid = r.id;
|
||||
if (!pid || !ids.has(pid)) {
|
||||
fail(`manifest policyProfileRefs contains unknown or missing id: "${pid}"`);
|
||||
}
|
||||
}
|
||||
for (const res of manifest.resources || []) {
|
||||
const pid = res.policyProfileId;
|
||||
if (pid && !ids.has(pid)) {
|
||||
fail(`resource ${res.resourceId} references unknown policyProfileId: "${pid}"`);
|
||||
}
|
||||
}
|
||||
console.log('[validate-ura-profiles] OK: manifest refs match registry');
|
||||
}
|
||||
@@ -188,6 +188,15 @@ else
|
||||
log_warn "Optional config/universal-resource-activation/policy-profiles.json missing; skipping"
|
||||
fi
|
||||
# Optional production gate: URA_STRICT_CLOSURE=1 fails if pilots/TBD/open reconciliation remain
|
||||
if [[ -f "$PROJECT_ROOT/config/universal-resource-activation/integration/omnl-ledger-mapping.v1.json" ]] && command -v node &>/dev/null && [[ -f "$PROJECT_ROOT/scripts/validate/validate-omnl-ledger-mapping.mjs" ]]; then
|
||||
log_ok "Found: config/universal-resource-activation/integration/omnl-ledger-mapping.v1.json"
|
||||
if node "$PROJECT_ROOT/scripts/validate/validate-omnl-ledger-mapping.mjs" "$PROJECT_ROOT/config/universal-resource-activation/integration/omnl-ledger-mapping.v1.json"; then
|
||||
log_ok "omnl-ledger-mapping.v1.json: JSON Schema OK"
|
||||
else
|
||||
log_err "omnl-ledger-mapping.v1.json: validation failed"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
if [[ "${URA_STRICT_CLOSURE:-}" == "1" ]] && [[ -f "$PROJECT_ROOT/scripts/ura/validate-manifest-closure.mjs" ]]; then
|
||||
log_info "URA_STRICT_CLOSURE=1: running URA manifest closure gate…"
|
||||
if node "$PROJECT_ROOT/scripts/ura/validate-manifest-closure.mjs" --strict; then
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Universal resource activation — local smoke: JSON Schema validation (always) +
|
||||
# optional HTTP GET to Phoenix (when --http or PHOENIX_BASE_URL is set).
|
||||
# optional HTTP GETs to Phoenix (when --http or PHOENIX_BASE_URL is set):
|
||||
# 1) GET /api/v1/universal-resource-activation/manifest (expect 200 + .schemaVersion)
|
||||
# 2) GET /api/v1/universal-resource-activation/policy-profiles (expect 200 + .profiles array)
|
||||
# 3) GET /api/v1/universal-resource-activation/server-funds-sidecar-probe
|
||||
# — expect 200 (sidecar ok or probe JSON) or 503 with .configured == false (URL unset)
|
||||
# — 502 = fail (URL set but sidecar paths not healthy)
|
||||
#
|
||||
# Usage (repo root):
|
||||
# bash scripts/verify/smoke-universal-resource-activation.sh
|
||||
@@ -61,7 +66,9 @@ fi
|
||||
|
||||
log "GET $URL (expect 200, JSON with .schemaVersion)…"
|
||||
body_file="$(mktemp)"
|
||||
trap 'rm -f "$body_file"' EXIT
|
||||
probe_file="$(mktemp)"
|
||||
profiles_file="$(mktemp)"
|
||||
trap 'rm -f "$body_file" "$probe_file" "$profiles_file"' EXIT
|
||||
code=$(curl -sS -o "$body_file" -w '%{http_code}' --connect-timeout 5 --max-time 15 "$URL" || true)
|
||||
|
||||
if [[ "$code" != "200" ]]; then
|
||||
@@ -78,4 +85,42 @@ if ! jq -e '.schemaVersion | type == "string"' "$body_file" &>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
log "HTTP OK (.schemaVersion present; HTTP $code)"
|
||||
|
||||
PROFILES_URL="${BASE}/api/v1/universal-resource-activation/policy-profiles"
|
||||
log "GET $PROFILES_URL (expect 200, JSON with .profiles array)…"
|
||||
prcode=$(curl -sS -o "$profiles_file" -w '%{http_code}' --connect-timeout 5 --max-time 15 "$PROFILES_URL" || true)
|
||||
if [[ "$prcode" != "200" ]]; then
|
||||
log_err "policy-profiles HTTP $prcode (expected 200). BASE=$BASE"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '(.profiles | type == "array")' "$profiles_file" &>/dev/null; then
|
||||
log_err "policy-profiles response missing .profiles array"
|
||||
cat "$profiles_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "policy-profiles HTTP OK"
|
||||
|
||||
PROBE_URL="${BASE}/api/v1/universal-resource-activation/server-funds-sidecar-probe"
|
||||
log "GET $PROBE_URL (expect 200 OK response or 503 with configured not true when URL unset)…"
|
||||
pcode=$(curl -sS -o "$probe_file" -w '%{http_code}' --connect-timeout 5 --max-time 20 "$PROBE_URL" || true)
|
||||
|
||||
if [[ "$pcode" == "200" ]]; then
|
||||
if ! jq -e 'type == "object"' "$probe_file" &>/dev/null; then
|
||||
log_err "Probe response is not a JSON object"
|
||||
exit 1
|
||||
fi
|
||||
log "sidecar-probe HTTP 200 (JSON object returned)"
|
||||
elif [[ "$pcode" == "503" ]]; then
|
||||
if ! jq -e '.configured == false' "$probe_file" &>/dev/null; then
|
||||
log_err "Probe expected 503 with .configured==false when SERVER_FUNDS_SIDECAR_URL unset; got: $(head -c 400 "$probe_file")"
|
||||
exit 1
|
||||
fi
|
||||
log "sidecar-probe HTTP 503 (SERVER_FUNDS_SIDECAR_URL not set — expected in dev)"
|
||||
elif [[ "$pcode" == "502" ]]; then
|
||||
log_err "sidecar-probe HTTP 502 — SERVER_FUNDS_SIDECAR_URL is set but sidecar health paths failed. Fix env or sidecar. Body: $(head -c 400 "$probe_file")"
|
||||
exit 1
|
||||
else
|
||||
log_err "sidecar-probe HTTP $pcode (expected 200, 503, or 502). Body: $(head -c 400 "$probe_file")"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user