firedrill-run 2.2.4 → 3.0.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/README.md +178 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +465 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1622 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +61 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { CliError, endpoint, jsonRequest, responseError, WORKOS_ORIGIN } from './http.js';
|
|
5
|
+
import { credentialLock, loadCredentials, removeCredentials, saveCredentials, loadPendingDevice, savePendingDevice, removePendingDevice, credentialMode, isCredentialCompanion, eraseVolatileCredentials, volatileCredentialSummary } from './credentials.js';
|
|
6
|
+
import { credentialStoreStatus } from './credential-mode.js';
|
|
7
|
+
import { companionRequest, startCompanionServer } from './native/companion-ipc.js';
|
|
8
|
+
import { chooseLoginOrganization, loginOrganizationIdentifier } from './login-organizations.js';
|
|
9
|
+
import { nativeSessionMatches, nativeMembershipSyncMatches, nativeWorkspacesMatch, nativeRevokedMatches, nativeOrganizationsMatch } from '../../contracts/native-identity-contracts.js';
|
|
10
|
+
async function configuration(signal) {
|
|
11
|
+
const result = await jsonRequest(endpoint('/api/auth/cli/config'), { signal });
|
|
12
|
+
if (result.status !== 200)
|
|
13
|
+
throw responseError(result.body, result.status);
|
|
14
|
+
const value = result.body;
|
|
15
|
+
try {
|
|
16
|
+
const origin = new URL(value.verification_origin);
|
|
17
|
+
if (value.schema !== 'firedrill.cli-auth.v1' || !/^client_[A-Za-z0-9]{1,120}$/.test(value.client_id)
|
|
18
|
+
|| origin.protocol !== 'https:' || origin.origin !== value.verification_origin || origin.username || origin.password
|
|
19
|
+
|| value.device_authorization_endpoint !== WORKOS_ORIGIN + '/user_management/authorize/device'
|
|
20
|
+
|| value.token_endpoint !== WORKOS_ORIGIN + '/user_management/authenticate')
|
|
21
|
+
throw new Error();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new CliError('auth_configuration_invalid', 'Terminal sign-in configuration is unavailable.');
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function organizationClaim(accessToken) {
|
|
29
|
+
const claims = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64url').toString('utf8'));
|
|
30
|
+
if (claims.org_id !== undefined && claims.org_id !== null && (typeof claims.org_id !== 'string' || !/^org_[A-Za-z0-9]{1,120}$/.test(claims.org_id)))
|
|
31
|
+
throw new Error();
|
|
32
|
+
return claims.org_id ?? undefined;
|
|
33
|
+
}
|
|
34
|
+
function credentials(body, config, previous, selectedOrganization) {
|
|
35
|
+
try {
|
|
36
|
+
const value = body;
|
|
37
|
+
if (typeof value.access_token !== 'string' || value.access_token.length > 16384 || typeof value.refresh_token !== 'string'
|
|
38
|
+
|| !value.refresh_token || value.refresh_token.length > 16384)
|
|
39
|
+
throw new Error();
|
|
40
|
+
// Expiry/identity here only organize local storage. The API independently
|
|
41
|
+
// verifies signature/issuer/client/session and current membership.
|
|
42
|
+
const claims = JSON.parse(Buffer.from(value.access_token.split('.')[1], 'base64url').toString('utf8'));
|
|
43
|
+
if (!/^user_[A-Za-z0-9]{1,120}$/.test(claims.sub) || !/^session_[A-Za-z0-9]{1,120}$/.test(claims.sid)
|
|
44
|
+
|| !Number.isSafeInteger(claims.exp) || claims.exp <= Date.now() / 1000
|
|
45
|
+
|| value.user?.id && value.user.id !== claims.sub
|
|
46
|
+
|| previous && (previous.subject !== claims.sub || previous.sid !== claims.sid || previous.client_id !== config.client_id))
|
|
47
|
+
throw new Error();
|
|
48
|
+
const organization = organizationClaim(value.access_token);
|
|
49
|
+
if (selectedOrganization !== undefined ? organization !== selectedOrganization
|
|
50
|
+
: previous && organization !== organizationClaim(previous.access_token))
|
|
51
|
+
throw new Error();
|
|
52
|
+
return { schema: 'firedrill.native-credentials.v1', state: 'ready', client_id: config.client_id,
|
|
53
|
+
access_token: value.access_token, refresh_token: value.refresh_token, subject: claims.sub, sid: claims.sid, expires_at: claims.exp };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new CliError('auth_response_invalid', 'Sign-in returned an incomplete response. Start sign-in again.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function openBrowser(url) {
|
|
60
|
+
return new Promise(resolve => {
|
|
61
|
+
const platform = process.platform;
|
|
62
|
+
const command = platform === 'darwin' ? '/usr/bin/open' : platform === 'win32' ? 'rundll32.exe' : 'xdg-open';
|
|
63
|
+
const args = platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
|
|
64
|
+
const child = spawn(command, args, { shell: false, stdio: 'ignore', windowsHide: true });
|
|
65
|
+
child.once('error', () => resolve(false));
|
|
66
|
+
child.once('exit', code => resolve(code === 0));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function tokenRequest(config, body, signal) {
|
|
70
|
+
return jsonRequest(new URL(config.token_endpoint), { method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ ...body, client_id: config.client_id }), signal });
|
|
72
|
+
}
|
|
73
|
+
/** Terminal (CLI) device authorization only. Remote MCP OAuth and agent registration are separate flows. */
|
|
74
|
+
export async function login(options) {
|
|
75
|
+
loginOrganizationIdentifier(options.organization);
|
|
76
|
+
const session = credentialMode() === 'session';
|
|
77
|
+
if (session && !isCredentialCompanion())
|
|
78
|
+
throw new CliError('session_login_requires_companion', 'Session-only sign-in happens inside the companion process. Run `firedrill init --session-only` and keep it running. No sign-in was started.', 2);
|
|
79
|
+
if (!session) {
|
|
80
|
+
// Report supported remediation before any browser ceremony. Never a file fallback.
|
|
81
|
+
const store = await credentialStoreStatus({ verifyWrite: true, signal: options.signal });
|
|
82
|
+
if (!store.available)
|
|
83
|
+
throw new CliError('credential_store_unavailable', `${store.remediation} No sign-in was started.`, 2);
|
|
84
|
+
}
|
|
85
|
+
const launchBrowser = options.open && (!options.json || options.explicitOpen === true);
|
|
86
|
+
return credentialLock(async (active) => {
|
|
87
|
+
options = { ...options, signal: AbortSignal.any([options.signal, active]) };
|
|
88
|
+
// Loading the native binding first refuses a missing store before asking
|
|
89
|
+
// the user to authorize. Actual read/write failures remain explicit.
|
|
90
|
+
const previous = await loadCredentials();
|
|
91
|
+
let pending = await loadPendingDevice();
|
|
92
|
+
options.signal.throwIfAborted();
|
|
93
|
+
const config = await configuration(options.signal);
|
|
94
|
+
async function finishAuthorization(value) {
|
|
95
|
+
let saved = value;
|
|
96
|
+
const organization = await chooseLoginOrganization(async (query, page, selected = '') => {
|
|
97
|
+
const params = new URLSearchParams({ q: query, page: String(page), ...(selected ? { organization_id: selected } : {}) });
|
|
98
|
+
const choices = await authorized('/api/auth/native/organizations?' + params, { signal: options.signal }, saved.access_token);
|
|
99
|
+
if (!nativeOrganizationsMatch(choices, saved.subject, query, page, selected))
|
|
100
|
+
throw new CliError('organization_unconfirmed', 'Organization access could not be confirmed. No workspace was selected.', 2);
|
|
101
|
+
return choices;
|
|
102
|
+
}, options);
|
|
103
|
+
if (organization && organization !== organizationClaim(saved.access_token)) {
|
|
104
|
+
await saveCredentials({ ...saved, state: 'refresh_pending' }, options.signal);
|
|
105
|
+
const switched = await tokenRequest(config, { grant_type: 'refresh_token', refresh_token: saved.refresh_token,
|
|
106
|
+
organization_id: organization }, options.signal);
|
|
107
|
+
if (switched.status !== 200)
|
|
108
|
+
throw new CliError('organization_authorization_required', 'The selected organization was not authorized. Complete its browser sign-in before trying again. No workspace access was granted.', 2);
|
|
109
|
+
saved = credentials(switched.body, config, saved, organization);
|
|
110
|
+
}
|
|
111
|
+
await saveCredentials({ ...saved, state: 'ready' }, options.signal);
|
|
112
|
+
return authorized('/api/auth/native/register', { method: 'POST', body: { label: 'Terminal' }, signal: options.signal }, saved.access_token);
|
|
113
|
+
}
|
|
114
|
+
if (previous?.state === 'organization_selection_pending' && previous.client_id === config.client_id && previous.expires_at > Date.now() / 1000 + 60) {
|
|
115
|
+
// The single-use exchange already returned and its exact credentials are
|
|
116
|
+
// retained. Resume organization selection, not a new device exchange.
|
|
117
|
+
await removePendingDevice(options.signal);
|
|
118
|
+
return finishAuthorization(previous);
|
|
119
|
+
}
|
|
120
|
+
if (pending && pending.expires_at <= Date.now() / 1000) {
|
|
121
|
+
if (pending.state === 'polling')
|
|
122
|
+
throw new CliError('authorization_exchange_uncertain', 'The previous authorization exchange was not confirmed. Review Account access in the browser. Use logout --local-only to clear its local attempt before starting another; this does not revoke an unknown cloud credential.', 2);
|
|
123
|
+
await removePendingDevice(options.signal);
|
|
124
|
+
pending = null;
|
|
125
|
+
}
|
|
126
|
+
if (pending && (pending.client_id !== config.client_id || new URL(pending.verification_url).origin !== config.verification_origin
|
|
127
|
+
|| pending.organization_hint !== (options.organization ?? null)))
|
|
128
|
+
throw new CliError('authorization_intent_changed', 'An original authorization is pending for different settings. Resume it with its original options or clear it with logout --local-only. No replacement was created.', 2);
|
|
129
|
+
if (pending?.state === 'polling')
|
|
130
|
+
throw new CliError('authorization_exchange_uncertain', 'The original token exchange was interrupted and cannot be safely repeated. Review Account access in the browser. Clear the local attempt with logout --local-only before starting another.', 2);
|
|
131
|
+
if (!pending) {
|
|
132
|
+
const grant = await jsonRequest(new URL(config.device_authorization_endpoint), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: config.client_id }), signal: options.signal });
|
|
133
|
+
if (grant.status !== 200)
|
|
134
|
+
throw new CliError('device_authorization_failed', 'Browser sign-in could not be started.');
|
|
135
|
+
const value = grant.body;
|
|
136
|
+
let link;
|
|
137
|
+
try {
|
|
138
|
+
const base = new URL(value.verification_uri);
|
|
139
|
+
link = new URL(value.verification_uri_complete ?? value.verification_uri);
|
|
140
|
+
if (base.origin !== config.verification_origin || link.origin !== config.verification_origin || base.pathname !== '/device'
|
|
141
|
+
|| link.pathname !== '/device' || link.username || link.password || link.hash || base.search || base.hash
|
|
142
|
+
|| typeof value.device_code !== 'string' || !value.device_code || value.device_code.length > 16384
|
|
143
|
+
|| !/^[A-Z0-9-]{4,64}$/.test(value.user_code) || !Number.isInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 3600
|
|
144
|
+
|| value.interval !== undefined && (!Number.isInteger(value.interval) || value.interval < 1 || value.interval > 300)
|
|
145
|
+
|| [...link.searchParams.keys()].some(key => key !== 'user_code')
|
|
146
|
+
|| link.search && link.searchParams.get('user_code') !== value.user_code)
|
|
147
|
+
throw new Error();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new CliError('device_authorization_invalid', 'The browser sign-in response was not accepted.');
|
|
151
|
+
}
|
|
152
|
+
const interval = value.interval ?? 5;
|
|
153
|
+
pending = { schema: 'firedrill.device-authorization.v1', id: randomUUID(), state: 'waiting', client_id: config.client_id,
|
|
154
|
+
organization_hint: options.organization ?? null, device_code: value.device_code, user_code: value.user_code,
|
|
155
|
+
verification_url: link.href, expires_at: Math.floor(Date.now() / 1000) + value.expires_in,
|
|
156
|
+
interval, next_poll_at: Date.now() + interval * 1000 };
|
|
157
|
+
await savePendingDevice(pending, options.signal);
|
|
158
|
+
}
|
|
159
|
+
const instruction = { type: 'authorization_required', authorization_id: pending.id, verification_url: pending.verification_url,
|
|
160
|
+
user_code: pending.user_code, expires_in: Math.max(0, pending.expires_at - Math.floor(Date.now() / 1000)), payment_required: false };
|
|
161
|
+
if (options.json)
|
|
162
|
+
process.stdout.write(JSON.stringify({ ...instruction, credential_mode: session ? 'session' : 'persistent' }) + '\n');
|
|
163
|
+
else
|
|
164
|
+
process.stderr.write(`Open ${pending.verification_url}\nCode: ${pending.user_code}\nNo credit card required. ${session
|
|
165
|
+
? 'Session-only: this sign-in stays in memory and ends when this companion stops; if interrupted, start sign-in again.'
|
|
166
|
+
: 'Run the same command to resume if interrupted.'}\n`);
|
|
167
|
+
if (launchBrowser)
|
|
168
|
+
await openBrowser(pending.verification_url);
|
|
169
|
+
while (Date.now() < pending.expires_at * 1000) {
|
|
170
|
+
await sleep(Math.min(Math.max(0, pending.next_poll_at - Date.now()), Math.max(0, pending.expires_at * 1000 - Date.now())), undefined, { signal: options.signal });
|
|
171
|
+
options.signal.throwIfAborted();
|
|
172
|
+
if (Date.now() >= pending.expires_at * 1000)
|
|
173
|
+
break;
|
|
174
|
+
pending = { ...pending, state: 'polling' };
|
|
175
|
+
await savePendingDevice(pending, options.signal);
|
|
176
|
+
const result = await tokenRequest(config, { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: pending.device_code }, options.signal);
|
|
177
|
+
if (result.status === 200) {
|
|
178
|
+
const saved = credentials(result.body, config);
|
|
179
|
+
await saveCredentials({ ...saved, state: 'organization_selection_pending' }, options.signal);
|
|
180
|
+
await removePendingDevice(options.signal);
|
|
181
|
+
// A lost registration reply is recoverable with this same retained
|
|
182
|
+
// provider session. Never exchange the single-use device code again.
|
|
183
|
+
return finishAuthorization(saved);
|
|
184
|
+
}
|
|
185
|
+
const error = result.body && typeof result.body === 'object' ? result.body.error : undefined;
|
|
186
|
+
if (error === 'authorization_pending' || error === 'slow_down') {
|
|
187
|
+
// RFC 8628: slow_down permanently adds 5 seconds to the polling interval.
|
|
188
|
+
const interval = error === 'slow_down' ? Math.min(3600, pending.interval + 5) : pending.interval;
|
|
189
|
+
pending = { ...pending, state: 'waiting', interval, next_poll_at: Date.now() + interval * 1000 };
|
|
190
|
+
await savePendingDevice(pending, options.signal);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// A definitive provider answer: the device code is spent or refused.
|
|
194
|
+
await removePendingDevice(options.signal);
|
|
195
|
+
if (error === 'access_denied')
|
|
196
|
+
throw new CliError('authorization_denied', 'Browser authorization was declined. No terminal access was granted.', 2);
|
|
197
|
+
if (error === 'expired_token')
|
|
198
|
+
throw new CliError('authorization_expired', 'Browser sign-in expired before it was approved. Run firedrill login again. No terminal access was granted.', 2);
|
|
199
|
+
throw new CliError('authorization_ended', 'Browser authorization ended. Start sign-in again. No terminal access was granted.', 2);
|
|
200
|
+
}
|
|
201
|
+
await removePendingDevice(options.signal);
|
|
202
|
+
throw new CliError('authorization_expired', 'Browser sign-in expired. Run firedrill login again.', 2);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function authorized(path, options, accessToken) {
|
|
206
|
+
if (options.bodyJson !== undefined && (typeof options.bodyJson !== 'string' || options.body !== undefined
|
|
207
|
+
|| Buffer.byteLength(options.bodyJson) > 8 * 1024 * 1024 + 65536)) {
|
|
208
|
+
throw new CliError('request_body_invalid', 'Provide one bounded JSON request body. Nothing was submitted.', 2);
|
|
209
|
+
}
|
|
210
|
+
if (options.idempotencyKey !== undefined && (typeof options.idempotencyKey !== 'string'
|
|
211
|
+
|| !/^[\x21-\x7e]{1,128}$/.test(options.idempotencyKey)))
|
|
212
|
+
throw new CliError('request_key_invalid', 'Use the saved request identifier.');
|
|
213
|
+
const route = endpoint(path), method = (options.method ?? 'GET').toUpperCase();
|
|
214
|
+
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
215
|
+
// Preserve the existing reviewed package pre-effect capacity exception;
|
|
216
|
+
// the broader projection-unavailable recovery below applies only to GET.
|
|
217
|
+
const packageOriginal = method === 'POST' && !route.search && (new RegExp(`^/api/workspaces/${uuid}/tools/package-requests$`).test(route.pathname)
|
|
218
|
+
&& typeof options.idempotencyKey === 'string' && new RegExp(`^${uuid}$`).test(options.idempotencyKey)
|
|
219
|
+
|| new RegExp(`^/api/workspaces/${uuid}/tools/package-requests/${uuid}/(?:upload-access|complete)$`).test(route.pathname));
|
|
220
|
+
const request = { method,
|
|
221
|
+
headers: { Authorization: `Bearer ${accessToken}`, ...(options.actor ? { 'X-Firedrill-Actor': options.actor } : {}),
|
|
222
|
+
...(options.workspace ? { 'X-Firedrill-Workspace': options.workspace } : {}),
|
|
223
|
+
...(options.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
|
|
224
|
+
...(options.body === undefined && options.bodyJson === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
225
|
+
body: options.bodyJson ?? (options.body === undefined ? undefined : JSON.stringify(options.body)), signal: options.signal };
|
|
226
|
+
// Projection refresh is asynchronous and can briefly outlast its freshness
|
|
227
|
+
// window. Repeat only an answered GET refusal, with exactly the same token,
|
|
228
|
+
// actor/workspace and path. No new write or credential-exchange retry scope.
|
|
229
|
+
let recoveryDeadline = performance.now() + 25_000;
|
|
230
|
+
const readSignal = method === 'GET'
|
|
231
|
+
? AbortSignal.any([...(options.signal ? [options.signal] : []), AbortSignal.timeout(25_000)])
|
|
232
|
+
: options.signal;
|
|
233
|
+
let result = await jsonRequest(route, { ...request, signal: readSignal }, options.maximumResponseBytes);
|
|
234
|
+
if (method !== 'GET')
|
|
235
|
+
recoveryDeadline = performance.now() + 10_000;
|
|
236
|
+
for (let attempt = 1; attempt < (method === 'GET' ? 9 : 3)
|
|
237
|
+
&& (method === 'GET' || method === 'HEAD' || packageOriginal); attempt += 1) {
|
|
238
|
+
if (result.status !== 503 || !(method === 'GET'
|
|
239
|
+
? ['identity_service_unavailable', 'identity_refresh_capacity', 'native_authorization_refresh_pending'] : ['identity_refresh_capacity'])
|
|
240
|
+
.includes(responseError(result.body, result.status).code))
|
|
241
|
+
break;
|
|
242
|
+
const wait = Math.max(result.retryAfterMs ?? 0, method === 'GET' ? Math.min(4_000, 500 * 2 ** (attempt - 1)) : 1_100)
|
|
243
|
+
+ Math.floor(Math.random() * 250);
|
|
244
|
+
const remaining = recoveryDeadline - performance.now();
|
|
245
|
+
// Never retry earlier than an advertised delay, or extend the finite
|
|
246
|
+
// recovery window. A network/parse/timeout failure below escapes unchanged.
|
|
247
|
+
if (wait >= remaining)
|
|
248
|
+
break;
|
|
249
|
+
readSignal?.throwIfAborted();
|
|
250
|
+
await sleep(wait, undefined, { signal: readSignal });
|
|
251
|
+
const budget = recoveryDeadline - performance.now();
|
|
252
|
+
if (budget <= 0)
|
|
253
|
+
break;
|
|
254
|
+
const signal = method === 'GET' ? readSignal
|
|
255
|
+
: AbortSignal.any([...(options.signal ? [options.signal] : []), AbortSignal.timeout(Math.ceil(budget))]);
|
|
256
|
+
result = await jsonRequest(route, { ...request, signal }, options.maximumResponseBytes);
|
|
257
|
+
}
|
|
258
|
+
if (result.status < 200 || result.status >= 300)
|
|
259
|
+
throw responseError(result.body, result.status);
|
|
260
|
+
let matches = true;
|
|
261
|
+
if (['/api/auth/native/register', '/api/auth/native/session'].includes(route.pathname) || route.pathname === '/api/auth/native/workspaces' && method === 'POST') {
|
|
262
|
+
const choice = options.body && typeof options.body === 'object' && 'workspace_id' in options.body ? options.body.workspace_id : undefined;
|
|
263
|
+
matches = nativeSessionMatches(result.body, { actor: options.actor, workspace: typeof choice === 'string' ? choice : undefined });
|
|
264
|
+
}
|
|
265
|
+
else if (route.pathname === '/api/auth/native/workspaces' && method === 'GET') {
|
|
266
|
+
matches = nativeWorkspacesMatch(result.body, options.actor, route.searchParams.get('q') ?? '', Number(route.searchParams.get('page') ?? '1'));
|
|
267
|
+
}
|
|
268
|
+
else if (route.pathname === '/api/auth/native/organizations' && method === 'GET') {
|
|
269
|
+
try {
|
|
270
|
+
const subject = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64url').toString('utf8')).sub;
|
|
271
|
+
matches = nativeOrganizationsMatch(result.body, subject, route.searchParams.get('q') ?? '', Number(route.searchParams.get('page') ?? '1'), route.searchParams.get('organization_id') ?? '', options.actor);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
matches = false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (route.pathname === '/api/auth/native/membership-sync')
|
|
278
|
+
matches = nativeMembershipSyncMatches(result.body);
|
|
279
|
+
else if (route.pathname === '/api/auth/native/revoke')
|
|
280
|
+
matches = nativeRevokedMatches(result.body, options.actor);
|
|
281
|
+
if (!matches)
|
|
282
|
+
throw new CliError('native_identity_unconfirmed', 'Terminal identity or original selected access could not be confirmed. No additional action was sent.');
|
|
283
|
+
return result.body;
|
|
284
|
+
}
|
|
285
|
+
async function readyCredentials(signal) {
|
|
286
|
+
signal?.throwIfAborted();
|
|
287
|
+
let saved = await loadCredentials();
|
|
288
|
+
const again = credentialMode() === 'session' ? 'Stop this companion and run firedrill init --session-only again' : 'Run firedrill login again';
|
|
289
|
+
if (!saved)
|
|
290
|
+
throw new CliError('authentication_required', credentialMode() === 'session'
|
|
291
|
+
? 'This session-only companion is not signed in. Stop it and run firedrill init --session-only.' : 'Run firedrill login to authorize this terminal.', 2);
|
|
292
|
+
if (saved.state === 'organization_selection_pending')
|
|
293
|
+
throw new CliError('organization_selection_required', `Organization selection was not completed. ${again}. No workspace access was granted.`, 2);
|
|
294
|
+
if (saved.state === 'refresh_pending')
|
|
295
|
+
throw new CliError('refresh_reconciliation_required', `A prior token refresh was not confirmed. ${again}; the old refresh token is not retried.`, 2);
|
|
296
|
+
if (saved.expires_at <= Date.now() / 1000 + 60) {
|
|
297
|
+
const config = await configuration(signal);
|
|
298
|
+
if (config.client_id !== saved.client_id)
|
|
299
|
+
throw new CliError('auth_configuration_changed', 'Sign in again to use this application.', 2);
|
|
300
|
+
await saveCredentials({ ...saved, state: 'refresh_pending' }, signal);
|
|
301
|
+
const organization = organizationClaim(saved.access_token);
|
|
302
|
+
const result = await tokenRequest(config, { grant_type: 'refresh_token', refresh_token: saved.refresh_token,
|
|
303
|
+
...(organization ? { organization_id: organization } : {}) }, signal);
|
|
304
|
+
if (result.status !== 200)
|
|
305
|
+
throw new CliError('refresh_not_confirmed', 'The refresh was not confirmed. Sign in again rather than retrying the old token.', 2);
|
|
306
|
+
saved = credentials(result.body, config, saved);
|
|
307
|
+
await saveCredentials(saved, signal);
|
|
308
|
+
}
|
|
309
|
+
return saved;
|
|
310
|
+
}
|
|
311
|
+
const SESSION_COMPANION_REQUIRED = 'No signed-in session-only companion is running. Run `firedrill run` or `firedrill init --session-only` in another terminal, or sign in with an OS credential store.';
|
|
312
|
+
/** A session-mode client process: credentials live in another (companion) process. */
|
|
313
|
+
function sessionClient() { return credentialMode() === 'session' && !isCredentialCompanion(); }
|
|
314
|
+
async function companionAccessToken(signal) {
|
|
315
|
+
try {
|
|
316
|
+
return (await companionRequest('access-token', signal)).access_token;
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (error instanceof CliError && error.code === 'companion_unavailable')
|
|
320
|
+
throw new CliError('session_companion_required', SESSION_COMPANION_REQUIRED, 2);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
let readingCredentials = null;
|
|
325
|
+
export async function access(work) {
|
|
326
|
+
// Session-mode clients never read a store: the companion refreshes in its own
|
|
327
|
+
// memory (refresh_pending first, no blind retry) and returns a short-lived
|
|
328
|
+
// access token over authenticated local IPC. Nothing is cached on disk.
|
|
329
|
+
if (sessionClient())
|
|
330
|
+
return work(await companionAccessToken());
|
|
331
|
+
// Serialize keyring/refresh intent, not unrelated business requests. Keeping
|
|
332
|
+
// the OS credential lock through evidence/model HTTP would prevent the same
|
|
333
|
+
// runner's presence keeper from maintaining its original finite lease.
|
|
334
|
+
const reading = readingCredentials ??= credentialLock(active => readyCredentials(active));
|
|
335
|
+
let credentials;
|
|
336
|
+
try {
|
|
337
|
+
credentials = await reading;
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
if (readingCredentials === reading)
|
|
341
|
+
readingCredentials = null;
|
|
342
|
+
}
|
|
343
|
+
return work(credentials.access_token);
|
|
344
|
+
}
|
|
345
|
+
export async function api(path, options = {}) {
|
|
346
|
+
return access(async (token) => {
|
|
347
|
+
try {
|
|
348
|
+
return await authorized(path, options, token);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (!(error instanceof CliError) || error.code !== 'native_session_required')
|
|
352
|
+
throw error;
|
|
353
|
+
await authorized('/api/auth/native/register', { method: 'POST', body: { label: 'Terminal' } }, token);
|
|
354
|
+
if (options.method && options.method !== 'GET')
|
|
355
|
+
throw new CliError('native_session_registered', 'Terminal registration is saved. Run the original command again; it did not pass authentication.');
|
|
356
|
+
return authorized(path, options, token);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
export async function logout(options) {
|
|
361
|
+
if (sessionClient()) {
|
|
362
|
+
// Revoke with the companion's current token, then stop the companion so its
|
|
363
|
+
// volatile credentials end. --local-only only stops the companion.
|
|
364
|
+
let revoked = false;
|
|
365
|
+
if (!options.localOnly) {
|
|
366
|
+
const result = await authorized('/api/auth/native/revoke', { method: 'POST' }, await companionAccessToken());
|
|
367
|
+
if (result.revoked !== true)
|
|
368
|
+
throw new CliError('revocation_unconfirmed', 'Terminal revocation was not confirmed. The companion was left running.');
|
|
369
|
+
revoked = true;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await companionRequest('shutdown');
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (!(error instanceof CliError && error.code === 'companion_unavailable'))
|
|
376
|
+
throw new CliError('companion_shutdown_unconfirmed', `${revoked ? 'Terminal access was revoked, but the' : 'The'} companion did not confirm shutdown. Stop it with Ctrl-C in its terminal.`, 2);
|
|
377
|
+
}
|
|
378
|
+
return { revoked, local_removed: true };
|
|
379
|
+
}
|
|
380
|
+
return credentialLock(async (active) => {
|
|
381
|
+
const saved = await loadCredentials();
|
|
382
|
+
const pending = await loadPendingDevice();
|
|
383
|
+
if (options.localOnly || !saved) {
|
|
384
|
+
await removePendingDevice(active);
|
|
385
|
+
await removeCredentials(active);
|
|
386
|
+
return { revoked: false, local_removed: true };
|
|
387
|
+
}
|
|
388
|
+
// A prior refresh can be uncertain while its old access token is still
|
|
389
|
+
// valid. Use that only to revoke its own existing session, never to refresh
|
|
390
|
+
// again or execute resource actions. Expired state needs browser recovery.
|
|
391
|
+
const current = saved.expires_at > Date.now() / 1000 + 5 ? saved : await readyCredentials(active);
|
|
392
|
+
const result = await authorized('/api/auth/native/revoke', { method: 'POST', signal: active }, current.access_token);
|
|
393
|
+
if (result.revoked !== true)
|
|
394
|
+
throw new CliError('revocation_unconfirmed', 'Terminal revocation was not confirmed.');
|
|
395
|
+
await removePendingDevice(active);
|
|
396
|
+
await removeCredentials(active);
|
|
397
|
+
return { revoked: pending?.state !== 'polling', local_removed: true };
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Start the companion IPC server in the process selected as companion by
|
|
402
|
+
* configureCredentialMode(). Session mode serves short-lived access tokens to
|
|
403
|
+
* same-user CLI processes; persistent mode serves status only (clients read the
|
|
404
|
+
* OS store themselves). Closing erases volatile credentials. The caller wires
|
|
405
|
+
* SIGINT/SIGTERM/SIGHUP to handle.close('signal').
|
|
406
|
+
*/
|
|
407
|
+
export async function startCredentialCompanion(options = {}) {
|
|
408
|
+
if (!isCredentialCompanion())
|
|
409
|
+
throw new CliError('companion_role_required', 'This process was not started as the Firedrill companion. No local access was opened.', 2);
|
|
410
|
+
const mode = credentialMode();
|
|
411
|
+
// Refreshes run under the companion's lifetime, not a client's request: a
|
|
412
|
+
// client that disconnects or times out must never strand refresh_pending or
|
|
413
|
+
// discard a token pair the identity provider already rotated.
|
|
414
|
+
const lifetime = new AbortController();
|
|
415
|
+
return startCompanionServer({ mode, onShutdown: reason => { lifetime.abort(); options.onShutdown?.(reason); }, handlers: {
|
|
416
|
+
status: async () => {
|
|
417
|
+
if (mode === 'session')
|
|
418
|
+
return volatileCredentialSummary();
|
|
419
|
+
const saved = await loadCredentials().catch(() => null);
|
|
420
|
+
return { signed_in: saved?.state === 'ready', state: saved?.state ?? null, subject: saved?.subject ?? null, expires_at: saved?.expires_at ?? null };
|
|
421
|
+
},
|
|
422
|
+
accessToken: async (signal) => {
|
|
423
|
+
if (mode !== 'session')
|
|
424
|
+
throw new CliError('companion_token_not_served', 'This companion uses the OS credential store; other commands read the sign-in from that store. No token was returned.', 2);
|
|
425
|
+
const work = credentialLock(active => readyCredentials(AbortSignal.any([lifetime.signal, active])));
|
|
426
|
+
// The request signal only abandons this reply; the refresh itself completes.
|
|
427
|
+
const ready = await new Promise((resolve, reject) => {
|
|
428
|
+
const onAbort = () => reject(new CliError('companion_cancelled', 'The request ended before a token was ready. No token was returned.'));
|
|
429
|
+
if (signal.aborted) {
|
|
430
|
+
onAbort();
|
|
431
|
+
work.catch(() => undefined);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
435
|
+
work.then(value => { signal.removeEventListener('abort', onAbort); resolve(value); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); });
|
|
436
|
+
});
|
|
437
|
+
return { access_token: ready.access_token, expires_at: ready.expires_at };
|
|
438
|
+
},
|
|
439
|
+
erase: () => { lifetime.abort(); if (mode === 'session')
|
|
440
|
+
eraseVolatileCredentials(); },
|
|
441
|
+
} });
|
|
442
|
+
}
|
|
443
|
+
/** Token-free credential summary for `firedrill status`. */
|
|
444
|
+
export async function credentialOverview(signal) {
|
|
445
|
+
const mode = credentialMode(), companionProcess = isCredentialCompanion();
|
|
446
|
+
let running = null;
|
|
447
|
+
if (!companionProcess) {
|
|
448
|
+
running = await companionRequest('status', signal).catch(error => {
|
|
449
|
+
if (error instanceof CliError && error.code === 'companion_unavailable')
|
|
450
|
+
return null;
|
|
451
|
+
throw error;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const companion = running ? { mode: running.mode, pid: running.pid, started_at: running.started_at, signed_in: running.signed_in } : null;
|
|
455
|
+
if (mode === 'session') {
|
|
456
|
+
const summary = companionProcess ? volatileCredentialSummary()
|
|
457
|
+
: { signed_in: running?.mode === 'session' && running.signed_in, state: running?.mode === 'session' ? running.state : null,
|
|
458
|
+
expires_at: running?.mode === 'session' ? running.expires_at : null };
|
|
459
|
+
return { mode, companion_process: companionProcess, signed_in: summary.signed_in, state: summary.state, expires_at: summary.expires_at, store: null, companion };
|
|
460
|
+
}
|
|
461
|
+
const store = await credentialStoreStatus({ signal });
|
|
462
|
+
const saved = store.available ? await loadCredentials().catch(() => null) : null;
|
|
463
|
+
return { mode, companion_process: companionProcess, signed_in: saved?.state === 'ready', state: saved?.state ?? null,
|
|
464
|
+
expires_at: saved?.expires_at ?? null, store, companion };
|
|
465
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/billing-commerce-contracts.js';
|