@agilno-tech/rivet 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProviderAdapterError,
|
|
3
|
+
boundedString,
|
|
4
|
+
captureRecord,
|
|
5
|
+
failProvider,
|
|
6
|
+
immutableRedactedJson,
|
|
7
|
+
isSensitiveQueryKey,
|
|
8
|
+
providerWireBytes,
|
|
9
|
+
} from './contract.js';
|
|
10
|
+
import { isIP } from 'node:net';
|
|
11
|
+
|
|
12
|
+
const METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
13
|
+
const HEADER = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
14
|
+
const FORBIDDEN_HEADERS = new Set(['connection', 'content-length', 'cookie', 'host', 'proxy-authorization', 'transfer-encoding']);
|
|
15
|
+
const TIMED_OUT = Symbol('provider-timeout');
|
|
16
|
+
const ABORTED = Symbol('provider-aborted');
|
|
17
|
+
const DNS_UNSAFE = Symbol('provider-dns-unsafe');
|
|
18
|
+
const TRANSPORT_FAILED = Symbol('provider-transport-failed');
|
|
19
|
+
const MALFORMED_READ = Symbol('provider-malformed-read');
|
|
20
|
+
const PROMISE_THEN = Promise.prototype.then;
|
|
21
|
+
const ABORTED_GET = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get;
|
|
22
|
+
const EVENT_ADD = EventTarget.prototype.addEventListener;
|
|
23
|
+
const EVENT_REMOVE = EventTarget.prototype.removeEventListener;
|
|
24
|
+
const trustedTransports = new WeakSet();
|
|
25
|
+
const responseMetadata = new WeakMap();
|
|
26
|
+
const PAGINATION_QUERY_KEYS = new Set(['page', 'per_page', 'startAt', 'maxResults', 'cursor', 'after', 'before']);
|
|
27
|
+
|
|
28
|
+
export function createTrustedProviderTransport(input) {
|
|
29
|
+
const value = captureRecord(input, new Set(['resolve', 'fetchPinned']), ['resolve', 'fetchPinned'], 'invalid-config');
|
|
30
|
+
if (typeof value.resolve !== 'function' || typeof value.fetchPinned !== 'function') failProvider('invalid-config');
|
|
31
|
+
const transport = Object.freeze({ resolve: value.resolve, fetchPinned: value.fetchPinned });
|
|
32
|
+
trustedTransports.add(transport);
|
|
33
|
+
return transport;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function integer(value, fallback, minimum, maximum) {
|
|
37
|
+
const result = value ?? fallback;
|
|
38
|
+
if (!Number.isSafeInteger(result) || result < minimum || result > maximum) failProvider('invalid-config');
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function originUrl(value) {
|
|
43
|
+
boundedString(value, 2048, undefined, 'invalid-config');
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(value);
|
|
46
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash
|
|
47
|
+
|| unsafeHostname(url.hostname) || isIP(url.hostname.replace(/^\[|\]$/g, '')) !== 0) failProvider('invalid-config');
|
|
48
|
+
url.pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
|
|
49
|
+
return url;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof ProviderAdapterError) throw error;
|
|
52
|
+
failProvider('invalid-config');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unsafeHostname(hostnameInput) {
|
|
57
|
+
const hostname = hostnameInput.toLowerCase().replace(/^\[|\]$/g, '');
|
|
58
|
+
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local')
|
|
59
|
+
|| hostname.endsWith('.internal') || hostname.endsWith('.lan') || hostname.includes(':')) return true;
|
|
60
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
61
|
+
if (!match) return false;
|
|
62
|
+
const octets = match.slice(1).map(Number);
|
|
63
|
+
if (octets.some(value => value > 255)) return true;
|
|
64
|
+
return octets[0] === 0 || octets[0] === 10 || octets[0] === 127 || octets[0] >= 224
|
|
65
|
+
|| (octets[0] === 169 && octets[1] === 254)
|
|
66
|
+
|| (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31)
|
|
67
|
+
|| (octets[0] === 192 && octets[1] === 168)
|
|
68
|
+
|| (octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function publicAddress(address) {
|
|
72
|
+
const version = isIP(address);
|
|
73
|
+
if (version === 4) {
|
|
74
|
+
const value = address.split('.').reduce((total, octet) => (total << 8n) | BigInt(octet), 0n);
|
|
75
|
+
const reserved = [
|
|
76
|
+
[0x00000000n, 8], [0x0a000000n, 8], [0x64400000n, 10], [0x7f000000n, 8],
|
|
77
|
+
[0xa9fe0000n, 16], [0xac100000n, 12], [0xc0000000n, 24], [0xc0000200n, 24],
|
|
78
|
+
[0xc0586300n, 24], [0xc0a80000n, 16], [0xc6120000n, 15], [0xc6336400n, 24],
|
|
79
|
+
[0xcb007100n, 24], [0xe0000000n, 4], [0xf0000000n, 4],
|
|
80
|
+
];
|
|
81
|
+
return !reserved.some(([prefix, length]) => prefixMatch(value, prefix, length, 32));
|
|
82
|
+
}
|
|
83
|
+
if (version === 6) {
|
|
84
|
+
const value = parseIpv6(address);
|
|
85
|
+
if (value === null || !prefixMatch(value, 0x20000000000000000000000000000000n, 3, 128)) return false;
|
|
86
|
+
return ![
|
|
87
|
+
[0x20010000000000000000000000000000n, 23],
|
|
88
|
+
[0x20010db8000000000000000000000000n, 32],
|
|
89
|
+
[0x20020000000000000000000000000000n, 16],
|
|
90
|
+
[0x3fff0000000000000000000000000000n, 20],
|
|
91
|
+
].some(([prefix, length]) => prefixMatch(value, prefix, length, 128));
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function canonicalPublicAddress(address) {
|
|
97
|
+
if (!publicAddress(address)) return null;
|
|
98
|
+
if (isIP(address) === 4) return address.split('.').map(Number).join('.');
|
|
99
|
+
const value = parseIpv6(address);
|
|
100
|
+
if (value === null) return null;
|
|
101
|
+
const groups = [];
|
|
102
|
+
for (let shift = 112n; shift >= 0n; shift -= 16n) groups.push(Number((value >> shift) & 0xffffn).toString(16));
|
|
103
|
+
let bestStart = -1;
|
|
104
|
+
let bestLength = 0;
|
|
105
|
+
for (let start = 0; start < groups.length;) {
|
|
106
|
+
if (groups[start] !== '0') { start += 1; continue; }
|
|
107
|
+
let end = start;
|
|
108
|
+
while (end < groups.length && groups[end] === '0') end += 1;
|
|
109
|
+
if (end - start > bestLength && end - start >= 2) { bestStart = start; bestLength = end - start; }
|
|
110
|
+
start = end;
|
|
111
|
+
}
|
|
112
|
+
if (bestStart < 0) return groups.join(':');
|
|
113
|
+
return `${groups.slice(0, bestStart).join(':')}::${groups.slice(bestStart + bestLength).join(':')}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function prefixMatch(value, prefix, length, bits) {
|
|
117
|
+
const shift = BigInt(bits - length);
|
|
118
|
+
return (value >> shift) === (prefix >> shift);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseIpv6(input) {
|
|
122
|
+
try {
|
|
123
|
+
let value = input.toLowerCase();
|
|
124
|
+
if (value.includes('.')) {
|
|
125
|
+
const split = value.lastIndexOf(':');
|
|
126
|
+
if (split < 0) return null;
|
|
127
|
+
const octets = value.slice(split + 1).split('.').map(Number);
|
|
128
|
+
if (octets.length !== 4 || octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) return null;
|
|
129
|
+
value = `${value.slice(0, split)}:${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
|
|
130
|
+
}
|
|
131
|
+
const halves = value.split('::');
|
|
132
|
+
if (halves.length > 2) return null;
|
|
133
|
+
const left = halves[0] ? halves[0].split(':') : [];
|
|
134
|
+
const right = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
|
|
135
|
+
const missing = 8 - left.length - right.length;
|
|
136
|
+
if ((halves.length === 1 && missing !== 0) || (halves.length === 2 && missing < 1)) return null;
|
|
137
|
+
const groups = [...left, ...Array(missing).fill('0'), ...right];
|
|
138
|
+
if (groups.length !== 8 || groups.some(group => !/^[a-f0-9]{1,4}$/.test(group))) return null;
|
|
139
|
+
return groups.reduce((total, group) => (total << 16n) | BigInt(`0x${group}`), 0n);
|
|
140
|
+
} catch { return null; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolvedAddresses(input, provider) {
|
|
144
|
+
try {
|
|
145
|
+
if (!Array.isArray(input) || Object.getPrototypeOf(input) !== Array.prototype) failProvider('dns-unsafe', { provider });
|
|
146
|
+
const length = input.length;
|
|
147
|
+
if (!Number.isSafeInteger(length) || length < 1 || length > 16) failProvider('dns-unsafe', { provider });
|
|
148
|
+
const snapshot = [];
|
|
149
|
+
for (let index = 0; index < length; index += 1) {
|
|
150
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, String(index));
|
|
151
|
+
if (!descriptor?.enumerable) failProvider('dns-unsafe', { provider });
|
|
152
|
+
snapshot.push(boundedString(input[index], 64, undefined, 'dns-unsafe'));
|
|
153
|
+
}
|
|
154
|
+
Object.freeze(snapshot);
|
|
155
|
+
const output = [];
|
|
156
|
+
for (const address of snapshot) {
|
|
157
|
+
const canonical = canonicalPublicAddress(address);
|
|
158
|
+
if (!canonical || output.includes(canonical)) failProvider('dns-unsafe', { provider });
|
|
159
|
+
output.push(canonical);
|
|
160
|
+
}
|
|
161
|
+
return Object.freeze(output.sort());
|
|
162
|
+
} catch { failProvider('dns-unsafe', { provider }); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function headers(input, reason = 'invalid-config') {
|
|
166
|
+
if (input === undefined) return Object.freeze({});
|
|
167
|
+
let array;
|
|
168
|
+
try { array = Array.isArray(input); } catch { failProvider(reason); }
|
|
169
|
+
if (!input || typeof input !== 'object' || array) failProvider(reason);
|
|
170
|
+
let keys;
|
|
171
|
+
try { keys = Reflect.ownKeys(input); } catch { failProvider(reason); }
|
|
172
|
+
if (keys.length > 64 || keys.some(key => typeof key !== 'string')) failProvider(reason);
|
|
173
|
+
const result = {};
|
|
174
|
+
for (const key of keys) {
|
|
175
|
+
let descriptor;
|
|
176
|
+
let entry;
|
|
177
|
+
try {
|
|
178
|
+
descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
179
|
+
entry = input[key];
|
|
180
|
+
} catch { failProvider(reason); }
|
|
181
|
+
if (!descriptor?.enumerable) failProvider(reason);
|
|
182
|
+
const name = boundedString(key.toLowerCase(), 64, HEADER, reason);
|
|
183
|
+
if (FORBIDDEN_HEADERS.has(name) || Object.hasOwn(result, name)) failProvider(reason);
|
|
184
|
+
result[name] = boundedString(entry, 8192, undefined, reason);
|
|
185
|
+
}
|
|
186
|
+
return Object.freeze(result);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function relayAbortSignal(input, provider) {
|
|
190
|
+
if (input === undefined) return Object.freeze({ signal: undefined, release() {} });
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const relay = () => { controller.abort(); };
|
|
193
|
+
let listening = false;
|
|
194
|
+
try {
|
|
195
|
+
Reflect.apply(EVENT_ADD, input, ['abort', relay, { once: true }]);
|
|
196
|
+
listening = true;
|
|
197
|
+
} catch { failProvider('invalid-request', { provider }); }
|
|
198
|
+
let aborted;
|
|
199
|
+
try { aborted = Reflect.apply(ABORTED_GET, input, []); } catch {
|
|
200
|
+
try { Reflect.apply(EVENT_REMOVE, input, ['abort', relay]); } catch {}
|
|
201
|
+
failProvider('invalid-request', { provider });
|
|
202
|
+
}
|
|
203
|
+
if (aborted) relay();
|
|
204
|
+
let released = false;
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
release() {
|
|
208
|
+
if (released) return;
|
|
209
|
+
released = true;
|
|
210
|
+
if (listening) {
|
|
211
|
+
listening = false;
|
|
212
|
+
try { Reflect.apply(EVENT_REMOVE, input, ['abort', relay]); } catch {}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function requestPath(value, allowEncodedSlash = false) {
|
|
219
|
+
boundedString(value, 4096);
|
|
220
|
+
if (!value.startsWith('/') || value.startsWith('//') || value.includes('\\')) failProvider('invalid-request');
|
|
221
|
+
const pathname = value.split('?', 1)[0];
|
|
222
|
+
if ((allowEncodedSlash ? /%(?:2e|5c|25)/i : /%(?:2e|2f|5c|25)/i).test(pathname)) failProvider('invalid-request');
|
|
223
|
+
const decoded = pathname.replace(/%2f/ig, '/');
|
|
224
|
+
if (decoded.includes('//') || decoded.split('/').some(part => part === '.' || part === '..')) failProvider('invalid-request');
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function retryFor(status) {
|
|
229
|
+
return status === 408 || status === 425 || status === 429 || status >= 500 ? 'transient' : 'permanent';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function responseHeaders(snapshot, provider) {
|
|
233
|
+
const values = {
|
|
234
|
+
etag: snapshot.etag, link: snapshot.link, retryAfter: snapshot.retryAfter,
|
|
235
|
+
};
|
|
236
|
+
for (const value of Object.values(values)) if (value !== null && (value.length > 8192 || /[\u0000-\u001f\u007f]/.test(value))) {
|
|
237
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
238
|
+
}
|
|
239
|
+
if (values.link !== null) values.link = sanitizedLinkHeader(values.link, provider);
|
|
240
|
+
return immutableRedactedJson(values);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function snapshotResponse(response, provider, baseUrl, basePrefix) {
|
|
244
|
+
try {
|
|
245
|
+
if (!(response instanceof Response)) throw new Error();
|
|
246
|
+
const redirected = response.redirected;
|
|
247
|
+
const responseUrlValue = response.url;
|
|
248
|
+
const status = response.status;
|
|
249
|
+
const ok = response.ok;
|
|
250
|
+
const responseHeaderObject = response.headers;
|
|
251
|
+
const body = response.body;
|
|
252
|
+
if (typeof redirected !== 'boolean' || redirected || typeof responseUrlValue !== 'string'
|
|
253
|
+
|| responseUrlValue.length > 2048 || !Number.isInteger(status) || status < 100 || status > 599
|
|
254
|
+
|| typeof ok !== 'boolean' || ok !== (status >= 200 && status < 300)
|
|
255
|
+
|| !responseHeaderObject || typeof responseHeaderObject !== 'object') throw new Error();
|
|
256
|
+
if (responseUrlValue) {
|
|
257
|
+
const responseUrl = new URL(responseUrlValue);
|
|
258
|
+
if (responseUrl.origin !== baseUrl.origin
|
|
259
|
+
|| (basePrefix && responseUrl.pathname !== basePrefix && !responseUrl.pathname.startsWith(`${basePrefix}/`))) throw new Error();
|
|
260
|
+
}
|
|
261
|
+
const get = responseHeaderObject.get;
|
|
262
|
+
if (typeof get !== 'function') throw new Error();
|
|
263
|
+
const headerValues = {};
|
|
264
|
+
for (const [field, name] of [
|
|
265
|
+
['contentLength', 'content-length'], ['etag', 'etag'], ['link', 'link'], ['retryAfter', 'retry-after'],
|
|
266
|
+
]) {
|
|
267
|
+
const value = Reflect.apply(get, responseHeaderObject, [name]);
|
|
268
|
+
if (value !== null && typeof value !== 'string') throw new Error();
|
|
269
|
+
headerValues[field] = value;
|
|
270
|
+
}
|
|
271
|
+
if (body !== null && (typeof body !== 'object' && typeof body !== 'function')) throw new Error();
|
|
272
|
+
return Object.freeze({ status, ok, body, headers: Object.freeze(headerValues) });
|
|
273
|
+
} catch { failProvider('remote', { provider, retryClassification: 'permanent' }); }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function snapshotReader(body, provider) {
|
|
277
|
+
try {
|
|
278
|
+
const getReader = body.getReader;
|
|
279
|
+
if (typeof getReader !== 'function') throw new Error();
|
|
280
|
+
const reader = Reflect.apply(getReader, body, []);
|
|
281
|
+
if (!reader || (typeof reader !== 'object' && typeof reader !== 'function')) throw new Error();
|
|
282
|
+
const read = reader.read;
|
|
283
|
+
if (typeof read !== 'function') throw new Error();
|
|
284
|
+
return Object.freeze({ reader, read });
|
|
285
|
+
} catch { failProvider('remote', { provider, retryClassification: 'permanent' }); }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function settleReaderCancellation(reader) {
|
|
289
|
+
try {
|
|
290
|
+
const cancel = reader.cancel;
|
|
291
|
+
if (typeof cancel !== 'function') return;
|
|
292
|
+
const pending = Reflect.apply(cancel, reader, []);
|
|
293
|
+
Reflect.apply(PROMISE_THEN, pending, [() => {}, () => {}]);
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function snapshotReadResult(value) {
|
|
298
|
+
try {
|
|
299
|
+
if (!value || typeof value !== 'object') return MALFORMED_READ;
|
|
300
|
+
const done = value.done;
|
|
301
|
+
if (typeof done !== 'boolean') return MALFORMED_READ;
|
|
302
|
+
return Object.freeze({ done, value: done ? undefined : value.value });
|
|
303
|
+
} catch { return MALFORMED_READ; }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sanitizedLinkHeader(value, provider) {
|
|
307
|
+
try {
|
|
308
|
+
return parseLinkHeader(value, provider).map(part => {
|
|
309
|
+
const url = new URL(part.url);
|
|
310
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
|
|
311
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
312
|
+
}
|
|
313
|
+
for (const key of [...url.searchParams.keys()]) if (isSensitiveQueryKey(key)) url.searchParams.delete(key);
|
|
314
|
+
return `<${url.href}>; rel="${part.rel}"`;
|
|
315
|
+
}).join(', ');
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof ProviderAdapterError) throw error;
|
|
318
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseLinkHeader(value, provider) {
|
|
323
|
+
const parts = value.split(',');
|
|
324
|
+
if (parts.length > 100) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
325
|
+
return parts.map(part => {
|
|
326
|
+
const match = /^\s*<([^>]+)>(.*)$/.exec(part);
|
|
327
|
+
if (!match) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
328
|
+
const parameters = [];
|
|
329
|
+
for (const raw of match[2].split(';').slice(1)) {
|
|
330
|
+
const parameter = /^\s*([A-Za-z][A-Za-z0-9_-]{0,63})\s*=\s*(?:"([^"]{1,256})"|([^\s;]{1,256}))\s*$/.exec(raw);
|
|
331
|
+
if (!parameter) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
332
|
+
parameters.push(Object.freeze({ name: parameter[1], value: parameter[2] ?? parameter[3] }));
|
|
333
|
+
}
|
|
334
|
+
const relations = parameters.filter(parameter => parameter.name.toLowerCase() === 'rel');
|
|
335
|
+
if (relations.length !== 1 || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(relations[0].value)) {
|
|
336
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
337
|
+
}
|
|
338
|
+
return Object.freeze({ url: match[1], rel: relations[0].value.toLowerCase(), parameters: Object.freeze(parameters) });
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function nextLink(link, provider) {
|
|
343
|
+
if (!link) return null;
|
|
344
|
+
for (const part of parseLinkHeader(link, provider)) {
|
|
345
|
+
if (part.rel === 'next') {
|
|
346
|
+
if (part.parameters.some(parameter => parameter.name.toLowerCase() !== 'rel' && isSensitiveQueryKey(parameter.name))) {
|
|
347
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
348
|
+
}
|
|
349
|
+
return part.url;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function createProviderHttpClient(input) {
|
|
356
|
+
const config = captureRecord(input, new Set([
|
|
357
|
+
'provider', 'baseUrl', 'transport', 'headers', 'timeoutMs', 'maxResponseBytes', 'maxPages', 'maxItems', 'allowEncodedSlash',
|
|
358
|
+
]), ['provider', 'baseUrl', 'transport'], 'invalid-config');
|
|
359
|
+
const provider = boundedString(config.provider, 32, /^[a-z][a-z0-9-]*$/, 'invalid-config');
|
|
360
|
+
if (!trustedTransports.has(config.transport)) failProvider('invalid-config', { provider });
|
|
361
|
+
if (config.allowEncodedSlash !== undefined && typeof config.allowEncodedSlash !== 'boolean') failProvider('invalid-config', { provider });
|
|
362
|
+
const allowEncodedSlash = config.allowEncodedSlash === true;
|
|
363
|
+
const transport = config.transport;
|
|
364
|
+
const baseUrl = originUrl(config.baseUrl);
|
|
365
|
+
const basePrefix = baseUrl.pathname === '/' ? '' : baseUrl.pathname;
|
|
366
|
+
const defaultHeaders = headers(config.headers);
|
|
367
|
+
const timeoutMs = integer(config.timeoutMs, 10_000, 1, 120_000);
|
|
368
|
+
const maxResponseBytes = integer(config.maxResponseBytes, 2 * 1024 * 1024, 1, 16 * 1024 * 1024);
|
|
369
|
+
const maxPages = integer(config.maxPages, 20, 1, 100);
|
|
370
|
+
const maxItems = integer(config.maxItems, 10_000, 1, 10_000);
|
|
371
|
+
|
|
372
|
+
function snapshotRequest(inputRequest) {
|
|
373
|
+
const value = captureRecord(inputRequest, new Set(['method', 'path', 'headers', 'wireBody', 'signal']), ['method', 'path']);
|
|
374
|
+
const method = boundedString(value.method, 8, /^[A-Z]+$/);
|
|
375
|
+
if (!METHODS.has(method)) failProvider('invalid-request', { provider });
|
|
376
|
+
const path = requestPath(value.path, allowEncodedSlash);
|
|
377
|
+
let url;
|
|
378
|
+
try {
|
|
379
|
+
url = new URL(`${basePrefix}${path}`, baseUrl.origin);
|
|
380
|
+
if (url.origin !== baseUrl.origin || url.username || url.password || url.hash
|
|
381
|
+
|| (basePrefix && url.pathname !== basePrefix && !url.pathname.startsWith(`${basePrefix}/`))) failProvider('invalid-request', { provider });
|
|
382
|
+
for (const key of url.searchParams.keys()) if (isSensitiveQueryKey(key)) failProvider('invalid-request', { provider });
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (error instanceof ProviderAdapterError) throw error;
|
|
385
|
+
failProvider('invalid-request', { provider });
|
|
386
|
+
}
|
|
387
|
+
const requestHeaders = { ...defaultHeaders, ...headers(value.headers, 'invalid-request') };
|
|
388
|
+
let body;
|
|
389
|
+
if (value.wireBody !== undefined) {
|
|
390
|
+
if (method === 'GET') failProvider('invalid-request', { provider });
|
|
391
|
+
try { body = providerWireBytes(value.wireBody); } catch { failProvider('invalid-request', { provider }); }
|
|
392
|
+
if (Buffer.byteLength(body) > maxResponseBytes) failProvider('invalid-request', { provider });
|
|
393
|
+
requestHeaders['content-type'] ??= 'application/json';
|
|
394
|
+
}
|
|
395
|
+
const signalState = relayAbortSignal(value.signal, provider);
|
|
396
|
+
return Object.freeze({
|
|
397
|
+
method, url: url.href, headers: Object.freeze(requestHeaders), body,
|
|
398
|
+
signal: signalState.signal, releaseSignal: signalState.release,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function perform(request) {
|
|
403
|
+
const controller = new AbortController();
|
|
404
|
+
let resolveAbort;
|
|
405
|
+
const cancelled = new Promise(resolve => { resolveAbort = resolve; });
|
|
406
|
+
const abort = () => { controller.abort(); resolveAbort(ABORTED); };
|
|
407
|
+
let timer;
|
|
408
|
+
let timedOut = false;
|
|
409
|
+
let listening = false;
|
|
410
|
+
try {
|
|
411
|
+
if (request.signal) {
|
|
412
|
+
try {
|
|
413
|
+
Reflect.apply(EVENT_ADD, request.signal, ['abort', abort, { once: true }]);
|
|
414
|
+
listening = true;
|
|
415
|
+
if (Reflect.apply(ABORTED_GET, request.signal, [])) abort();
|
|
416
|
+
} catch { failProvider('invalid-request', { provider }); }
|
|
417
|
+
if (Reflect.apply(ABORTED_GET, controller.signal, [])) {
|
|
418
|
+
failProvider('aborted', { provider, retryClassification: 'none' });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const timeout = new Promise(resolve => {
|
|
422
|
+
timer = setTimeout(() => {
|
|
423
|
+
timedOut = true;
|
|
424
|
+
controller.abort();
|
|
425
|
+
resolve(TIMED_OUT);
|
|
426
|
+
}, timeoutMs);
|
|
427
|
+
});
|
|
428
|
+
const operation = Promise.resolve().then(() => transport.resolve(baseUrl.hostname)).then(addresses => {
|
|
429
|
+
let pinnedAddresses;
|
|
430
|
+
try { pinnedAddresses = resolvedAddresses(addresses, provider); }
|
|
431
|
+
catch { return DNS_UNSAFE; }
|
|
432
|
+
return Promise.resolve().then(() => {
|
|
433
|
+
// DNS can settle after a timeout/abort. Never start a provider request then.
|
|
434
|
+
if (Reflect.apply(ABORTED_GET, controller.signal, [])) return ABORTED;
|
|
435
|
+
return transport.fetchPinned(request.url, {
|
|
436
|
+
method: request.method, headers: request.headers, ...(request.body === undefined ? {} : { body: request.body }),
|
|
437
|
+
signal: controller.signal, redirect: 'error', credentials: 'omit',
|
|
438
|
+
}, Object.freeze({ hostname: baseUrl.hostname, addresses: pinnedAddresses }));
|
|
439
|
+
}).then(value => value === ABORTED ? ABORTED : Object.freeze({ response: value }), () => TRANSPORT_FAILED);
|
|
440
|
+
}, () => TRANSPORT_FAILED);
|
|
441
|
+
const outcome = await Promise.race([operation, timeout, cancelled]);
|
|
442
|
+
if (outcome === ABORTED) failProvider('aborted', { provider, retryClassification: 'none' });
|
|
443
|
+
if (outcome === TIMED_OUT || timedOut) failProvider('timeout', { provider, retryClassification: 'transient' });
|
|
444
|
+
if (outcome === DNS_UNSAFE) failProvider('dns-unsafe', { provider, retryClassification: 'permanent' });
|
|
445
|
+
if (outcome === TRANSPORT_FAILED) {
|
|
446
|
+
if (request.signal && Reflect.apply(ABORTED_GET, request.signal, [])) {
|
|
447
|
+
failProvider('aborted', { provider, retryClassification: 'none' });
|
|
448
|
+
}
|
|
449
|
+
failProvider('transport', { provider, retryClassification: 'transient' });
|
|
450
|
+
}
|
|
451
|
+
const response = snapshotResponse(outcome.response, provider, baseUrl, basePrefix);
|
|
452
|
+
const declaredLength = response.headers.contentLength;
|
|
453
|
+
if (declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maxResponseBytes)) {
|
|
454
|
+
failProvider('response-too-large', { provider, retryClassification: 'permanent' });
|
|
455
|
+
}
|
|
456
|
+
const chunks = [];
|
|
457
|
+
let byteLength = 0;
|
|
458
|
+
if (response.body) {
|
|
459
|
+
const readerState = snapshotReader(response.body, provider);
|
|
460
|
+
for (let reads = 0; ; reads += 1) {
|
|
461
|
+
if (reads >= 10_000) {
|
|
462
|
+
settleReaderCancellation(readerState.reader);
|
|
463
|
+
failProvider('response-too-large', { provider, retryClassification: 'permanent' });
|
|
464
|
+
}
|
|
465
|
+
const bodyResult = await Promise.race([
|
|
466
|
+
Promise.resolve().then(() => Reflect.apply(readerState.read, readerState.reader, [])).then(
|
|
467
|
+
value => ({ value: snapshotReadResult(value) }), () => ({ bodyError: true }),
|
|
468
|
+
),
|
|
469
|
+
timeout, cancelled,
|
|
470
|
+
]);
|
|
471
|
+
if (bodyResult === ABORTED) {
|
|
472
|
+
settleReaderCancellation(readerState.reader);
|
|
473
|
+
failProvider('aborted', { provider, retryClassification: 'none' });
|
|
474
|
+
}
|
|
475
|
+
if (bodyResult === TIMED_OUT || timedOut) {
|
|
476
|
+
settleReaderCancellation(readerState.reader);
|
|
477
|
+
failProvider('timeout', { provider, retryClassification: 'transient' });
|
|
478
|
+
}
|
|
479
|
+
if (bodyResult?.bodyError) {
|
|
480
|
+
settleReaderCancellation(readerState.reader);
|
|
481
|
+
failProvider('transport', { provider, retryClassification: 'transient' });
|
|
482
|
+
}
|
|
483
|
+
if (bodyResult.value === MALFORMED_READ) {
|
|
484
|
+
settleReaderCancellation(readerState.reader);
|
|
485
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
486
|
+
}
|
|
487
|
+
if (bodyResult.value.done) break;
|
|
488
|
+
const chunk = bodyResult.value.value;
|
|
489
|
+
let chunkCopy;
|
|
490
|
+
try {
|
|
491
|
+
if (!(chunk instanceof Uint8Array)) throw new Error();
|
|
492
|
+
chunkCopy = Buffer.from(chunk);
|
|
493
|
+
} catch {
|
|
494
|
+
settleReaderCancellation(readerState.reader);
|
|
495
|
+
failProvider('transport', { provider, retryClassification: 'transient' });
|
|
496
|
+
}
|
|
497
|
+
byteLength += chunkCopy.byteLength;
|
|
498
|
+
if (!Number.isSafeInteger(byteLength) || byteLength > maxResponseBytes) {
|
|
499
|
+
settleReaderCancellation(readerState.reader);
|
|
500
|
+
failProvider('response-too-large', { provider, retryClassification: 'permanent' });
|
|
501
|
+
}
|
|
502
|
+
chunks.push(chunkCopy);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const bytes = Buffer.concat(chunks, byteLength);
|
|
506
|
+
let data = null;
|
|
507
|
+
if (bytes.byteLength > 0) {
|
|
508
|
+
try { data = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
|
|
509
|
+
catch {
|
|
510
|
+
failProvider('remote', {
|
|
511
|
+
provider, status: response.status,
|
|
512
|
+
retryClassification: response.ok ? 'permanent' : retryFor(response.status),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (!response.ok) failProvider('remote', { provider, status: response.status, retryClassification: retryFor(response.status) });
|
|
517
|
+
const result = Object.freeze({ status: response.status, data: immutableRedactedJson(data), headers: responseHeaders(response.headers, provider) });
|
|
518
|
+
responseMetadata.set(result, { link: response.headers.link });
|
|
519
|
+
return result;
|
|
520
|
+
} finally {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
if (listening) {
|
|
523
|
+
try { Reflect.apply(EVENT_REMOVE, request.signal, ['abort', abort]); } catch {}
|
|
524
|
+
}
|
|
525
|
+
try { request.releaseSignal(); } catch {}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function request(requestInput) { return perform(snapshotRequest(requestInput)); }
|
|
530
|
+
|
|
531
|
+
function urlFor(pathInput) {
|
|
532
|
+
const path = requestPath(pathInput, allowEncodedSlash);
|
|
533
|
+
const url = new URL(`${basePrefix}${path}`, baseUrl.origin);
|
|
534
|
+
for (const key of [...url.searchParams.keys()]) if (isSensitiveQueryKey(key)) url.searchParams.delete(key);
|
|
535
|
+
return url.href;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function paginate(inputPagination) {
|
|
539
|
+
const value = captureRecord(inputPagination, new Set([
|
|
540
|
+
'path', 'signal', 'itemsKey', 'mode', 'identityKey', 'totalKey',
|
|
541
|
+
]), ['path']);
|
|
542
|
+
let path = requestPath(value.path, allowEncodedSlash);
|
|
543
|
+
const itemsKey = value.itemsKey === undefined ? undefined
|
|
544
|
+
: boundedString(value.itemsKey, 64, /^[A-Za-z][A-Za-z0-9_]*$/);
|
|
545
|
+
const identityKey = value.identityKey === undefined ? undefined
|
|
546
|
+
: boundedString(value.identityKey, 64, /^[A-Za-z][A-Za-z0-9_]*$/);
|
|
547
|
+
const totalKey = value.totalKey === undefined ? undefined
|
|
548
|
+
: boundedString(value.totalKey, 64, /^[A-Za-z][A-Za-z0-9_]*$/);
|
|
549
|
+
const mode = value.mode === undefined ? 'link' : boundedString(value.mode, 16, /^(?:link|jira-offset)$/);
|
|
550
|
+
const initialUrl = new URL(`${basePrefix}${path}`, baseUrl.origin);
|
|
551
|
+
const endpointPathname = initialUrl.pathname;
|
|
552
|
+
const fixedQuery = new Map();
|
|
553
|
+
for (const key of new Set(initialUrl.searchParams.keys())) {
|
|
554
|
+
if (!PAGINATION_QUERY_KEYS.has(key)) fixedQuery.set(key, initialUrl.searchParams.getAll(key));
|
|
555
|
+
}
|
|
556
|
+
function validateNextUrl(url) {
|
|
557
|
+
if (url.origin !== baseUrl.origin || url.username || url.password || url.hash || url.pathname !== endpointPathname) {
|
|
558
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
559
|
+
}
|
|
560
|
+
const keys = [...new Set(url.searchParams.keys())];
|
|
561
|
+
if (keys.length > 32) failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
562
|
+
for (const key of keys) {
|
|
563
|
+
if (isSensitiveQueryKey(key)) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
564
|
+
const values = url.searchParams.getAll(key);
|
|
565
|
+
if (values.length !== 1) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
566
|
+
if (!PAGINATION_QUERY_KEYS.has(key)) {
|
|
567
|
+
if (JSON.stringify(values) !== JSON.stringify(fixedQuery.get(key))) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
568
|
+
} else if (key === 'page' && (!/^[1-9][0-9]{0,8}$/.test(values[0]))) failProvider('remote', { provider });
|
|
569
|
+
else if (key === 'per_page' && (!/^\d{1,3}$/.test(values[0]) || Number(values[0]) < 1 || Number(values[0]) > 100)) failProvider('remote', { provider });
|
|
570
|
+
else if (key === 'startAt' && (!/^\d{1,9}$/.test(values[0]))) failProvider('remote', { provider });
|
|
571
|
+
else if (key === 'maxResults' && (!/^[1-9]\d{0,3}$/.test(values[0]))) failProvider('remote', { provider });
|
|
572
|
+
else if (['cursor', 'after', 'before'].includes(key) && !/^[A-Za-z0-9._~+=/-]{1,512}$/.test(values[0])) failProvider('remote', { provider });
|
|
573
|
+
}
|
|
574
|
+
for (const [key, values] of fixedQuery) {
|
|
575
|
+
if (JSON.stringify(url.searchParams.getAll(key)) !== JSON.stringify(values)) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
validateNextUrl(initialUrl);
|
|
579
|
+
let expectedOffset;
|
|
580
|
+
let requestedMaxResults;
|
|
581
|
+
let previousTotal;
|
|
582
|
+
let currentPage = initialUrl.searchParams.has('page') ? Number(initialUrl.searchParams.get('page')) : 1;
|
|
583
|
+
const pinnedPageSize = initialUrl.searchParams.get('per_page');
|
|
584
|
+
const initialCursorKeys = ['cursor', 'after', 'before'].filter(key => initialUrl.searchParams.has(key));
|
|
585
|
+
if (mode === 'link' && initialCursorKeys.length > 1) failProvider('invalid-request', { provider });
|
|
586
|
+
let cursorKey = initialCursorKeys[0];
|
|
587
|
+
let currentCursor = cursorKey === undefined ? undefined : initialUrl.searchParams.get(cursorKey);
|
|
588
|
+
if (mode === 'jira-offset') {
|
|
589
|
+
const value = initialUrl.searchParams.get('startAt');
|
|
590
|
+
if (initialUrl.searchParams.getAll('startAt').length !== 1 || value === null || !/^\d{1,9}$/.test(value)) {
|
|
591
|
+
failProvider('invalid-request', { provider });
|
|
592
|
+
}
|
|
593
|
+
expectedOffset = Number(value);
|
|
594
|
+
const maximum = initialUrl.searchParams.get('maxResults');
|
|
595
|
+
if (initialUrl.searchParams.getAll('maxResults').length !== 1 || maximum === null || !/^[1-9]\d{0,3}$/.test(maximum)) {
|
|
596
|
+
failProvider('invalid-request', { provider });
|
|
597
|
+
}
|
|
598
|
+
requestedMaxResults = Number(maximum);
|
|
599
|
+
}
|
|
600
|
+
const output = [];
|
|
601
|
+
const seen = new Set();
|
|
602
|
+
const identities = new Set();
|
|
603
|
+
let declaredTotal;
|
|
604
|
+
let totalDeclaration;
|
|
605
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
606
|
+
if (seen.has(path)) failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
607
|
+
seen.add(path);
|
|
608
|
+
const response = await request({ method: 'GET', path, ...(value.signal === undefined ? {} : { signal: value.signal }) });
|
|
609
|
+
const pageItems = itemsKey === undefined ? response.data : response.data?.[itemsKey];
|
|
610
|
+
if (!Array.isArray(pageItems)) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
611
|
+
if (output.length + pageItems.length > maxItems) failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
612
|
+
if (identityKey !== undefined) {
|
|
613
|
+
for (const item of pageItems) {
|
|
614
|
+
const identity = item?.[identityKey];
|
|
615
|
+
const token = typeof identity === 'string' && identity.length > 0 && identity.length <= 256
|
|
616
|
+
? `s:${identity}`
|
|
617
|
+
: Number.isSafeInteger(identity) && identity >= 0 ? `n:${identity}` : null;
|
|
618
|
+
if (token === null || identities.has(token)) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
619
|
+
identities.add(token);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (totalKey !== undefined) {
|
|
623
|
+
const hasTotal = response.data !== null && typeof response.data === 'object'
|
|
624
|
+
&& Object.hasOwn(response.data, totalKey);
|
|
625
|
+
if (totalDeclaration === undefined) totalDeclaration = hasTotal;
|
|
626
|
+
else if (totalDeclaration !== hasTotal) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
627
|
+
if (hasTotal) {
|
|
628
|
+
const total = response.data[totalKey];
|
|
629
|
+
if (!Number.isSafeInteger(total) || total < 0) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
630
|
+
if (total > maxItems) failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
631
|
+
if (declaredTotal !== undefined && total !== declaredTotal) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
632
|
+
declaredTotal = total;
|
|
633
|
+
if (output.length + pageItems.length > total) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
let next;
|
|
637
|
+
if (mode === 'jira-offset') {
|
|
638
|
+
const { startAt, maxResults, total } = response.data ?? {};
|
|
639
|
+
if (Number.isSafeInteger(total) && total > maxItems) {
|
|
640
|
+
failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
641
|
+
}
|
|
642
|
+
if (!Number.isSafeInteger(startAt) || startAt < 0 || !Number.isSafeInteger(maxResults) || maxResults < 1
|
|
643
|
+
|| !Number.isSafeInteger(total) || total < 0 || startAt !== expectedOffset
|
|
644
|
+
|| maxResults > requestedMaxResults || pageItems.length > maxResults || startAt + pageItems.length > total
|
|
645
|
+
|| (previousTotal !== undefined && total < previousTotal)) {
|
|
646
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
647
|
+
}
|
|
648
|
+
previousTotal = total;
|
|
649
|
+
output.push(...pageItems);
|
|
650
|
+
if (startAt + pageItems.length >= total) return Object.freeze(output);
|
|
651
|
+
if (pageItems.length === 0) failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
652
|
+
expectedOffset = startAt + pageItems.length;
|
|
653
|
+
const current = new URL(`${basePrefix}${path}`, baseUrl.origin);
|
|
654
|
+
current.searchParams.set('startAt', String(expectedOffset));
|
|
655
|
+
next = current.href;
|
|
656
|
+
} else {
|
|
657
|
+
output.push(...pageItems);
|
|
658
|
+
next = nextLink(responseMetadata.get(response)?.link, provider);
|
|
659
|
+
if (!next) {
|
|
660
|
+
if (declaredTotal !== undefined && output.length !== declaredTotal) {
|
|
661
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
662
|
+
}
|
|
663
|
+
return Object.freeze(output);
|
|
664
|
+
}
|
|
665
|
+
if (declaredTotal !== undefined && output.length >= declaredTotal) {
|
|
666
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
let url;
|
|
670
|
+
try {
|
|
671
|
+
url = new URL(next);
|
|
672
|
+
validateNextUrl(url);
|
|
673
|
+
if (mode === 'link') {
|
|
674
|
+
if (url.searchParams.get('per_page') !== pinnedPageSize
|
|
675
|
+
|| url.searchParams.getAll('per_page').length !== (pinnedPageSize === null ? 0 : 1)) {
|
|
676
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
677
|
+
}
|
|
678
|
+
const nextCursorKeys = ['cursor', 'after', 'before'].filter(key => url.searchParams.has(key));
|
|
679
|
+
if (cursorKey !== undefined || nextCursorKeys.length > 0) {
|
|
680
|
+
if (nextCursorKeys.length !== 1 || (cursorKey !== undefined && nextCursorKeys[0] !== cursorKey)
|
|
681
|
+
|| url.searchParams.has('page')) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
682
|
+
cursorKey ??= nextCursorKeys[0];
|
|
683
|
+
const nextCursor = url.searchParams.get(cursorKey);
|
|
684
|
+
if (nextCursor === currentCursor) failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
685
|
+
currentCursor = nextCursor;
|
|
686
|
+
} else {
|
|
687
|
+
const nextPage = Number(url.searchParams.get('page'));
|
|
688
|
+
if (url.searchParams.getAll('page').length !== 1 || nextPage !== currentPage + 1) {
|
|
689
|
+
failProvider('remote', { provider, retryClassification: 'permanent' });
|
|
690
|
+
}
|
|
691
|
+
currentPage = nextPage;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (error instanceof ProviderAdapterError) throw error;
|
|
696
|
+
failProvider('remote', { provider });
|
|
697
|
+
}
|
|
698
|
+
path = `${basePrefix ? url.pathname.slice(basePrefix.length) : url.pathname}${url.search}`;
|
|
699
|
+
}
|
|
700
|
+
failProvider('pagination-limit', { provider, retryClassification: 'permanent' });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return Object.freeze({ provider, baseUrl: `${baseUrl.origin}${basePrefix}`, request, paginate, urlFor });
|
|
704
|
+
}
|