diff --git a/analytics-api-license-usage-guard/README.md b/analytics-api-license-usage-guard/README.md new file mode 100644 index 00000000..43ec2d96 --- /dev/null +++ b/analytics-api-license-usage-guard/README.md @@ -0,0 +1,33 @@ +# Analytics API License Usage Guard + +This module provides a focused Revenue Infrastructure slice for SCIBASE issue +#20. It validates paid analytics API customers before graph metadata queries are +served or billed. + +## What It Covers + +- License tier and active-license checks. +- Dataset and topic entitlement checks. +- Anonymization threshold enforcement for metadata-only exports. +- Private-content blocking. +- Monthly quota and overage authorization checks. +- Per-minute burst throttling. +- Deterministic revenue and audit packets for usage dashboards. + +## Run + +```bash +npm run check +npm test +npm run demo +``` + +Generated artifacts are written to `reports/`: + +- `summary.json` +- `reviewer-packet.md` +- `summary.svg` +- `demo.mp4` + +All data is synthetic and does not call Stripe, PayPal, customer systems, live +billing providers, or private analytics stores. diff --git a/analytics-api-license-usage-guard/acceptance-notes.md b/analytics-api-license-usage-guard/acceptance-notes.md new file mode 100644 index 00000000..e0202e7b --- /dev/null +++ b/analytics-api-license-usage-guard/acceptance-notes.md @@ -0,0 +1,20 @@ +# Acceptance Notes + +The guard classifies analytics API requests as: + +- `allow`: query is licensed, anonymous, inside quota, and billable. +- `bill_overage`: query can be served with authorized overage billing. +- `throttle`: burst or quota limits prevent immediate service. +- `block`: inactive license, disallowed package/topic, private content, or + anonymization failure. + +Validation commands: + +```bash +npm run check +npm test +npm run demo +ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height,pix_fmt -of default=noprint_wrappers=1 reports/demo.mp4 +git diff --check +git diff --cached --check +``` diff --git a/analytics-api-license-usage-guard/demo.js b/analytics-api-license-usage-guard/demo.js new file mode 100644 index 00000000..657fd11d --- /dev/null +++ b/analytics-api-license-usage-guard/demo.js @@ -0,0 +1,31 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { + analyzeAnalyticsApiUsage, + renderMarkdownReport, + renderSvgSummary, +} = require("./index"); +const { sampleAnalyticsApiPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const result = analyzeAnalyticsApiUsage(sampleAnalyticsApiPacket, { + asOf: "2026-05-22T12:00:00.000Z", +}); + +fs.writeFileSync( + path.join(reportsDir, "summary.json"), + `${JSON.stringify(result, null, 2)}\n` +); +fs.writeFileSync( + path.join(reportsDir, "reviewer-packet.md"), + renderMarkdownReport(result) +); +fs.writeFileSync( + path.join(reportsDir, "summary.svg"), + renderSvgSummary(result) +); + +console.log("analytics API license usage guard demo artifacts written"); +console.log(`audit digest: ${result.auditDigest}`); diff --git a/analytics-api-license-usage-guard/index.js b/analytics-api-license-usage-guard/index.js new file mode 100644 index 00000000..94825f28 --- /dev/null +++ b/analytics-api-license-usage-guard/index.js @@ -0,0 +1,310 @@ +const crypto = require("node:crypto"); + +const ALLOW = "allow"; +const BILL_OVERAGE = "bill_overage"; +const THROTTLE = "throttle"; +const BLOCK = "block"; + +function requireFields(value, fields, label) { + for (const field of fields) { + if (value[field] === undefined || value[field] === null || value[field] === "") { + throw new Error(`missing required ${label} field: ${field}`); + } + } +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + + return JSON.stringify(value); +} + +function createAuditDigest(result) { + const payload = { + asOf: result.asOf, + totals: result.totals, + decisions: result.decisions.map((decision) => ({ + id: decision.id, + customerId: decision.customerId, + status: decision.status, + billableUnits: decision.billableUnits, + reasons: decision.reasons, + })), + }; + + return crypto.createHash("sha256").update(stableStringify(payload)).digest("hex"); +} + +function licenseLookup(licenses) { + const lookup = new Map(); + for (const license of licenses) { + requireFields( + license, + [ + "customerId", + "tier", + "active", + "monthlyQuota", + "usedThisMonth", + "burstLimitPerMinute", + "allowedDatasets", + "allowedTopics", + "minimumCohortSize", + "overageAuthorized", + ], + "license" + ); + lookup.set(license.customerId, license); + } + return lookup; +} + +function evaluateRequest(request, licenses) { + requireFields( + request, + [ + "id", + "customerId", + "dataset", + "topic", + "estimatedRows", + "cohortSize", + "queryUnits", + "requestsLastMinute", + "includesPrivateContent", + "requestedAt", + ], + "analytics request" + ); + + const license = licenses.get(request.customerId); + const reasons = []; + const actions = []; + const base = { + id: request.id, + customerId: request.customerId, + dataset: request.dataset, + topic: request.topic, + queryUnits: Number(request.queryUnits), + billableUnits: 0, + status: BLOCK, + reasons, + actions, + }; + + if (!license) { + return { + ...base, + reasons: ["customer has no analytics API license"], + actions: ["Block query and route account to revenue operations"], + }; + } + + if (!license.active) { + reasons.push("analytics API license is inactive"); + } + if (!license.allowedDatasets.includes(request.dataset)) { + reasons.push("dataset is not included in the licensed analytics package"); + } + if (!license.allowedTopics.includes(request.topic)) { + reasons.push("topic is outside licensed usage scope"); + } + if (request.includesPrivateContent) { + reasons.push("query attempts to access private content rather than anonymized metadata"); + } + if (request.cohortSize < license.minimumCohortSize) { + reasons.push("cohort size is below anonymization threshold"); + } + + if (reasons.length > 0) { + return { + ...base, + status: BLOCK, + reasons, + actions: [ + "Block analytics response", + "Log revenue and privacy audit event", + "Show permitted datasets, topics, and anonymization requirements", + ], + }; + } + + if (request.requestsLastMinute > license.burstLimitPerMinute) { + return { + ...base, + status: THROTTLE, + reasons: ["request burst exceeds licensed per-minute API limit"], + actions: [ + "Throttle query before execution", + "Preserve request for API usage dashboard", + ], + }; + } + + const projectedUsage = Number(license.usedThisMonth) + Number(request.queryUnits); + if (projectedUsage > Number(license.monthlyQuota)) { + if (!license.overageAuthorized) { + return { + ...base, + status: THROTTLE, + reasons: ["monthly query quota would be exceeded and overage is not authorized"], + actions: [ + "Throttle query until renewal or account approval", + "Offer overage authorization workflow", + ], + }; + } + + return { + ...base, + status: BILL_OVERAGE, + billableUnits: base.queryUnits, + reasons: ["query exceeds quota but overage billing is authorized"], + actions: [ + "Serve anonymized analytics response", + "Record overage units for invoice line item", + ], + }; + } + + return { + ...base, + status: ALLOW, + billableUnits: base.queryUnits, + reasons: ["query is inside license, privacy, quota, and burst limits"], + actions: [ + "Serve anonymized analytics response", + "Record billable usage units", + ], + }; +} + +function analyzeAnalyticsApiUsage(packet, options = {}) { + requireFields(packet, ["licenses", "requests"], "analytics API packet"); + if (!Array.isArray(packet.licenses) || !Array.isArray(packet.requests)) { + throw new Error("licenses and requests must be arrays"); + } + + const licenses = licenseLookup(packet.licenses); + const decisions = packet.requests.map((request) => evaluateRequest(request, licenses)); + const totals = decisions.reduce( + (acc, decision) => { + acc.totalRequests += 1; + acc.billableUnits += decision.billableUnits; + acc.byStatus[decision.status] = (acc.byStatus[decision.status] || 0) + 1; + return acc; + }, + { + totalRequests: 0, + billableUnits: 0, + byStatus: {}, + } + ); + const result = { + asOf: options.asOf || packet.asOf || new Date().toISOString(), + totals, + decisions, + }; + + return { + ...result, + auditDigest: createAuditDigest(result), + }; +} + +function renderMarkdownReport(result) { + const lines = [ + "# Analytics API License Usage Guard", + "", + `As of: ${result.asOf}`, + `Audit digest: \`${result.auditDigest}\``, + "", + "## Totals", + "", + `- Requests evaluated: ${result.totals.totalRequests}`, + `- Billable units: ${result.totals.billableUnits}`, + ]; + + for (const [status, count] of Object.entries(result.totals.byStatus).sort()) { + lines.push(`- ${status}: ${count}`); + } + + lines.push("", "## Query Decisions", ""); + for (const decision of result.decisions) { + lines.push( + `### ${decision.id}`, + "", + `- Customer: ${decision.customerId}`, + `- Dataset: ${decision.dataset}`, + `- Topic: ${decision.topic}`, + `- Status: ${decision.status}`, + `- Billable units: ${decision.billableUnits}`, + `- Reasons: ${decision.reasons.join("; ")}`, + `- Actions: ${decision.actions.join("; ")}`, + "" + ); + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderSvgSummary(result) { + const rows = Object.entries(result.totals.byStatus) + .sort() + .map(([status, count], index) => { + const y = 154 + index * 46; + const width = Math.max(44, count * 92); + return `${escapeXml(status)} + + ${count}`; + }) + .join("\n "); + + return ` + + + + Analytics API License Usage Guard + Requests ${result.totals.totalRequests} | Billable units ${result.totals.billableUnits} + ${rows} + audit ${escapeXml(result.auditDigest.slice(0, 48))} + +`; +} + +module.exports = { + ALLOW, + BILL_OVERAGE, + THROTTLE, + BLOCK, + analyzeAnalyticsApiUsage, + createAuditDigest, + renderMarkdownReport, + renderSvgSummary, +}; diff --git a/analytics-api-license-usage-guard/package.json b/analytics-api-license-usage-guard/package.json new file mode 100644 index 00000000..abb0f1e2 --- /dev/null +++ b/analytics-api-license-usage-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "analytics-api-license-usage-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic analytics API license usage guard for SCIBASE issue #20.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js", + "test": "node test.js", + "demo": "node demo.js" + }, + "license": "MIT" +} diff --git a/analytics-api-license-usage-guard/reports/demo.mp4 b/analytics-api-license-usage-guard/reports/demo.mp4 new file mode 100644 index 00000000..82071f02 Binary files /dev/null and b/analytics-api-license-usage-guard/reports/demo.mp4 differ diff --git a/analytics-api-license-usage-guard/reports/reviewer-packet.md b/analytics-api-license-usage-guard/reports/reviewer-packet.md new file mode 100644 index 00000000..1039245d --- /dev/null +++ b/analytics-api-license-usage-guard/reports/reviewer-packet.md @@ -0,0 +1,84 @@ +# Analytics API License Usage Guard + +As of: 2026-05-22T12:00:00.000Z +Audit digest: `56f75fde23be7ba664e9e122c1c43530a39beeb8dc3bb96db3981a20cff230f5` + +## Totals + +- Requests evaluated: 7 +- Billable units: 120 +- allow: 1 +- block: 4 +- throttle: 2 + +## Query Decisions + +### query-001 + +- Customer: cust-nih-policy +- Dataset: citation_networks +- Topic: public_health +- Status: allow +- Billable units: 120 +- Reasons: query is inside license, privacy, quota, and burst limits +- Actions: Serve anonymized analytics response; Record billable usage units + +### query-002 + +- Customer: cust-market-intel +- Dataset: dataset_reuse +- Topic: biotech +- Status: block +- Billable units: 0 +- Reasons: dataset is not included in the licensed analytics package +- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements + +### query-003 + +- Customer: cust-market-intel +- Dataset: topic_trends +- Topic: biotech +- Status: block +- Billable units: 0 +- Reasons: cohort size is below anonymization threshold +- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements + +### query-004 + +- Customer: cust-market-intel +- Dataset: method_usage +- Topic: materials +- Status: throttle +- Billable units: 0 +- Reasons: request burst exceeds licensed per-minute API limit +- Actions: Throttle query before execution; Preserve request for API usage dashboard + +### query-005 + +- Customer: cust-market-intel +- Dataset: method_usage +- Topic: materials +- Status: throttle +- Billable units: 0 +- Reasons: monthly query quota would be exceeded and overage is not authorized +- Actions: Throttle query until renewal or account approval; Offer overage authorization workflow + +### query-006 + +- Customer: cust-expired-consortium +- Dataset: citation_networks +- Topic: open_science +- Status: block +- Billable units: 0 +- Reasons: analytics API license is inactive +- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements + +### query-007 + +- Customer: cust-nih-policy +- Dataset: dataset_reuse +- Topic: grant_outcomes +- Status: block +- Billable units: 0 +- Reasons: query attempts to access private content rather than anonymized metadata +- Actions: Block analytics response; Log revenue and privacy audit event; Show permitted datasets, topics, and anonymization requirements diff --git a/analytics-api-license-usage-guard/reports/summary.json b/analytics-api-license-usage-guard/reports/summary.json new file mode 100644 index 00000000..d1a710af --- /dev/null +++ b/analytics-api-license-usage-guard/reports/summary.json @@ -0,0 +1,131 @@ +{ + "asOf": "2026-05-22T12:00:00.000Z", + "totals": { + "totalRequests": 7, + "billableUnits": 120, + "byStatus": { + "allow": 1, + "block": 4, + "throttle": 2 + } + }, + "decisions": [ + { + "id": "query-001", + "customerId": "cust-nih-policy", + "dataset": "citation_networks", + "topic": "public_health", + "queryUnits": 120, + "billableUnits": 120, + "status": "allow", + "reasons": [ + "query is inside license, privacy, quota, and burst limits" + ], + "actions": [ + "Serve anonymized analytics response", + "Record billable usage units" + ] + }, + { + "id": "query-002", + "customerId": "cust-market-intel", + "dataset": "dataset_reuse", + "topic": "biotech", + "queryUnits": 70, + "billableUnits": 0, + "status": "block", + "reasons": [ + "dataset is not included in the licensed analytics package" + ], + "actions": [ + "Block analytics response", + "Log revenue and privacy audit event", + "Show permitted datasets, topics, and anonymization requirements" + ] + }, + { + "id": "query-003", + "customerId": "cust-market-intel", + "dataset": "topic_trends", + "topic": "biotech", + "queryUnits": 60, + "billableUnits": 0, + "status": "block", + "reasons": [ + "cohort size is below anonymization threshold" + ], + "actions": [ + "Block analytics response", + "Log revenue and privacy audit event", + "Show permitted datasets, topics, and anonymization requirements" + ] + }, + { + "id": "query-004", + "customerId": "cust-market-intel", + "dataset": "method_usage", + "topic": "materials", + "queryUnits": 40, + "billableUnits": 0, + "status": "throttle", + "reasons": [ + "request burst exceeds licensed per-minute API limit" + ], + "actions": [ + "Throttle query before execution", + "Preserve request for API usage dashboard" + ] + }, + { + "id": "query-005", + "customerId": "cust-market-intel", + "dataset": "method_usage", + "topic": "materials", + "queryUnits": 75, + "billableUnits": 0, + "status": "throttle", + "reasons": [ + "monthly query quota would be exceeded and overage is not authorized" + ], + "actions": [ + "Throttle query until renewal or account approval", + "Offer overage authorization workflow" + ] + }, + { + "id": "query-006", + "customerId": "cust-expired-consortium", + "dataset": "citation_networks", + "topic": "open_science", + "queryUnits": 35, + "billableUnits": 0, + "status": "block", + "reasons": [ + "analytics API license is inactive" + ], + "actions": [ + "Block analytics response", + "Log revenue and privacy audit event", + "Show permitted datasets, topics, and anonymization requirements" + ] + }, + { + "id": "query-007", + "customerId": "cust-nih-policy", + "dataset": "dataset_reuse", + "topic": "grant_outcomes", + "queryUnits": 130, + "billableUnits": 0, + "status": "block", + "reasons": [ + "query attempts to access private content rather than anonymized metadata" + ], + "actions": [ + "Block analytics response", + "Log revenue and privacy audit event", + "Show permitted datasets, topics, and anonymization requirements" + ] + } + ], + "auditDigest": "56f75fde23be7ba664e9e122c1c43530a39beeb8dc3bb96db3981a20cff230f5" +} diff --git a/analytics-api-license-usage-guard/reports/summary.svg b/analytics-api-license-usage-guard/reports/summary.svg new file mode 100644 index 00000000..e7a81804 --- /dev/null +++ b/analytics-api-license-usage-guard/reports/summary.svg @@ -0,0 +1,26 @@ + + + + + Analytics API License Usage Guard + Requests 7 | Billable units 120 + allow + + 1 + block + + 4 + throttle + + 2 + audit 56f75fde23be7ba664e9e122c1c43530a39beeb8dc3bb96d + diff --git a/analytics-api-license-usage-guard/requirements-map.md b/analytics-api-license-usage-guard/requirements-map.md new file mode 100644 index 00000000..f211037d --- /dev/null +++ b/analytics-api-license-usage-guard/requirements-map.md @@ -0,0 +1,18 @@ +# Requirements Map + +| Issue #20 area | Coverage | +| --- | --- | +| Licensing APIs & Analytics | Validates licensed metadata API query access before serving graph analytics. | +| Usage-based pricing | Records billable query units and blocks unapproved overage. | +| Real-time usage meters | Applies monthly quota and burst-limit enforcement. | +| Data licensing models | Enforces dataset, topic, and anonymized-metadata restrictions. | +| Secure payment integrations | Avoids live payment systems and emits audit packets that can feed finance review. | + +## Non-overlap + +This is not a generic billing, metering ledger, tax, dispute, SLA, royalty, +forecast, FX, plan migration, procurement, invoice acceptance, +sanctions/export-control, payment failover/webhook, trial/promotion, storage +overage, account transfer, collections, or prepaid compute credit breakage +slice. It focuses on paid analytics API license usage and query entitlement +gating. diff --git a/analytics-api-license-usage-guard/sample-data.js b/analytics-api-license-usage-guard/sample-data.js new file mode 100644 index 00000000..7c73fd63 --- /dev/null +++ b/analytics-api-license-usage-guard/sample-data.js @@ -0,0 +1,129 @@ +const sampleAnalyticsApiPacket = { + asOf: "2026-05-22T12:00:00.000Z", + licenses: [ + { + customerId: "cust-nih-policy", + tier: "government_policy", + active: true, + monthlyQuota: 10000, + usedThisMonth: 4200, + burstLimitPerMinute: 120, + allowedDatasets: ["citation_networks", "dataset_reuse", "reproducibility_scores"], + allowedTopics: ["public_health", "open_science", "grant_outcomes"], + minimumCohortSize: 25, + overageAuthorized: true, + }, + { + customerId: "cust-market-intel", + tier: "market_intelligence", + active: true, + monthlyQuota: 2500, + usedThisMonth: 2480, + burstLimitPerMinute: 40, + allowedDatasets: ["topic_trends", "method_usage"], + allowedTopics: ["materials", "biotech"], + minimumCohortSize: 50, + overageAuthorized: false, + }, + { + customerId: "cust-expired-consortium", + tier: "academic_consortium", + active: false, + monthlyQuota: 5000, + usedThisMonth: 1200, + burstLimitPerMinute: 80, + allowedDatasets: ["citation_networks"], + allowedTopics: ["open_science"], + minimumCohortSize: 30, + overageAuthorized: false, + }, + ], + requests: [ + { + id: "query-001", + customerId: "cust-nih-policy", + dataset: "citation_networks", + topic: "public_health", + estimatedRows: 800, + cohortSize: 80, + queryUnits: 120, + requestsLastMinute: 16, + includesPrivateContent: false, + requestedAt: "2026-05-22T10:00:00.000Z", + }, + { + id: "query-002", + customerId: "cust-market-intel", + dataset: "dataset_reuse", + topic: "biotech", + estimatedRows: 200, + cohortSize: 80, + queryUnits: 70, + requestsLastMinute: 18, + includesPrivateContent: false, + requestedAt: "2026-05-22T10:04:00.000Z", + }, + { + id: "query-003", + customerId: "cust-market-intel", + dataset: "topic_trends", + topic: "biotech", + estimatedRows: 140, + cohortSize: 18, + queryUnits: 60, + requestsLastMinute: 12, + includesPrivateContent: false, + requestedAt: "2026-05-22T10:08:00.000Z", + }, + { + id: "query-004", + customerId: "cust-market-intel", + dataset: "method_usage", + topic: "materials", + estimatedRows: 100, + cohortSize: 65, + queryUnits: 40, + requestsLastMinute: 52, + includesPrivateContent: false, + requestedAt: "2026-05-22T10:10:00.000Z", + }, + { + id: "query-005", + customerId: "cust-market-intel", + dataset: "method_usage", + topic: "materials", + estimatedRows: 250, + cohortSize: 65, + queryUnits: 75, + requestsLastMinute: 20, + includesPrivateContent: false, + requestedAt: "2026-05-22T10:12:00.000Z", + }, + { + id: "query-006", + customerId: "cust-expired-consortium", + dataset: "citation_networks", + topic: "open_science", + estimatedRows: 150, + cohortSize: 90, + queryUnits: 35, + requestsLastMinute: 5, + includesPrivateContent: false, + requestedAt: "2026-05-22T10:14:00.000Z", + }, + { + id: "query-007", + customerId: "cust-nih-policy", + dataset: "dataset_reuse", + topic: "grant_outcomes", + estimatedRows: 120, + cohortSize: 64, + queryUnits: 130, + requestsLastMinute: 10, + includesPrivateContent: true, + requestedAt: "2026-05-22T10:16:00.000Z", + }, + ], +}; + +module.exports = { sampleAnalyticsApiPacket }; diff --git a/analytics-api-license-usage-guard/test.js b/analytics-api-license-usage-guard/test.js new file mode 100644 index 00000000..78126d39 --- /dev/null +++ b/analytics-api-license-usage-guard/test.js @@ -0,0 +1,52 @@ +const assert = require("node:assert/strict"); +const { + ALLOW, + BLOCK, + THROTTLE, + analyzeAnalyticsApiUsage, + createAuditDigest, + renderMarkdownReport, + renderSvgSummary, +} = require("./index"); +const { sampleAnalyticsApiPacket } = require("./sample-data"); + +const result = analyzeAnalyticsApiUsage(sampleAnalyticsApiPacket); + +assert.equal(result.totals.totalRequests, 7); +assert.equal(result.totals.billableUnits, 120); +assert.equal(result.totals.byStatus[ALLOW], 1); +assert.equal(result.totals.byStatus[BLOCK], 4); +assert.equal(result.totals.byStatus[THROTTLE], 2); + +const allowed = result.decisions.find((decision) => decision.id === "query-001"); +assert.equal(allowed.status, ALLOW); +assert.equal(allowed.billableUnits, 120); + +const datasetBlock = result.decisions.find((decision) => decision.id === "query-002"); +assert.equal(datasetBlock.status, BLOCK); +assert.match(datasetBlock.reasons.join(" "), /not included/); + +const cohortBlock = result.decisions.find((decision) => decision.id === "query-003"); +assert.equal(cohortBlock.status, BLOCK); +assert.match(cohortBlock.reasons.join(" "), /anonymization threshold/); + +const burstThrottle = result.decisions.find((decision) => decision.id === "query-004"); +assert.equal(burstThrottle.status, THROTTLE); + +const quotaThrottle = result.decisions.find((decision) => decision.id === "query-005"); +assert.equal(quotaThrottle.status, THROTTLE); + +const privateBlock = result.decisions.find((decision) => decision.id === "query-007"); +assert.equal(privateBlock.status, BLOCK); +assert.match(privateBlock.reasons.join(" "), /private content/); + +assert.equal(createAuditDigest(result), result.auditDigest); +assert.match(renderMarkdownReport(result), /Analytics API License Usage Guard/); +assert.match(renderSvgSummary(result), /Billable units 120/); + +assert.throws( + () => analyzeAnalyticsApiUsage({ licenses: [], requests: [{}] }), + /missing required analytics request field: id/ +); + +console.log("analytics API license usage guard tests passed");