@mmerterden/multi-agent-pipeline 20.33.0 → 20.35.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/CHANGELOG.md +98 -1
- package/README.md +3 -1
- package/README.tr.md +2 -1
- package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
- package/docs/adr/README.md +2 -1
- package/docs/facts.json +5 -4
- package/docs/features.md +1 -1
- package/install/_dev-only-files.mjs +1 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +24 -2
- package/install/templates/claude-hooks.json +34 -1
- package/manifest.json +393 -339
- package/package.json +3 -1
- package/pipeline/agents/bulk-reader.md +3 -0
- package/pipeline/agents/code-reviewer.md +9 -0
- package/pipeline/agents/explorer.md +4 -0
- package/pipeline/agents/plan-critic.md +4 -2
- package/pipeline/commands/multi-agent/SKILL.md +17 -136
- package/pipeline/commands/multi-agent/channels/SKILL.md +16 -298
- package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/office-on/SKILL.md +7 -3
- package/pipeline/commands/multi-agent/office-status/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
- package/pipeline/commands/multi-agent/review/SKILL.md +7 -131
- package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
- package/pipeline/contract/CHANGELOG.md +9 -0
- package/pipeline/contract/README.md +6 -2
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/manifest.json +14 -1
- package/pipeline/contract/types/index.d.ts +24 -2
- package/pipeline/lib/json-file-lock.mjs +61 -15
- package/pipeline/lib/office-remote.mjs +228 -0
- package/pipeline/lib/qr-code.mjs +360 -0
- package/pipeline/lib/repo-hygiene.sh +1 -0
- package/pipeline/lib/run-completion.mjs +17 -0
- package/pipeline/multi-agent-refs/channels/generator.md +146 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -1
- package/pipeline/multi-agent-refs/channels/menu.md +55 -0
- package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
- package/pipeline/multi-agent-refs/channels/preview.md +25 -0
- package/pipeline/multi-agent-refs/channels/reference.md +98 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/office-remote-protocol.md +166 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +2 -2
- package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
- package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +25 -1
- package/pipeline/multi-agent-refs/features/verify-by-test.md +10 -0
- package/pipeline/multi-agent-refs/keychain.md +7 -0
- package/pipeline/multi-agent-refs/knowledge.md +9 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +8 -10
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +18 -20
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +20 -14
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +8 -7
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +6 -3
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/resume/details.md +35 -0
- package/pipeline/multi-agent-refs/review/inputs.md +87 -0
- package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
- package/pipeline/multi-agent-refs/review/valven.md +42 -0
- package/pipeline/multi-agent-refs/rules.md +3 -2
- package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
- package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
- package/pipeline/multi-agent-refs/setup/language.md +46 -0
- package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
- package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/office/NOTICE.md +4 -1
- package/pipeline/office/assets/char_12.png +0 -0
- package/pipeline/office/assets/char_13.png +0 -0
- package/pipeline/office/assets/char_14.png +0 -0
- package/pipeline/office/assets/char_15.png +0 -0
- package/pipeline/office/assets/char_16.png +0 -0
- package/pipeline/office/assets/char_17.png +0 -0
- package/pipeline/office/assets/char_18.png +0 -0
- package/pipeline/office/assets/char_19.png +0 -0
- package/pipeline/office/assets/char_20.png +0 -0
- package/pipeline/office/assets/char_21.png +0 -0
- package/pipeline/office/assets/char_22.png +0 -0
- package/pipeline/office/assets/char_23.png +0 -0
- package/pipeline/office/assets/char_24.png +0 -0
- package/pipeline/office/assets/char_25.png +0 -0
- package/pipeline/office/assets/char_26.png +0 -0
- package/pipeline/office/assets/char_27.png +0 -0
- package/pipeline/office/assets/char_28.png +0 -0
- package/pipeline/office/assets/char_29.png +0 -0
- package/pipeline/office/assets/char_30.png +0 -0
- package/pipeline/office/index.html +304 -55
- package/pipeline/office/toolkit.json +1 -1
- package/pipeline/rules/response-shape.md +6 -7
- package/pipeline/schemas/agent-state.schema.json +36 -0
- package/pipeline/schemas/phases.json +4 -4
- package/pipeline/schemas/plan-critique.schema.json +9 -2
- package/pipeline/schemas/planning-output.schema.json +43 -2
- package/pipeline/schemas/pr-reach.schema.json +55 -0
- package/pipeline/schemas/prefs.schema.json +37 -8
- package/pipeline/schemas/reviewer-output.schema.json +20 -2
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/schemas/triage-output.schema.json +32 -3
- package/pipeline/schemas/unattended-policy.json +2 -1
- package/pipeline/scripts/_graph-analysis.mjs +270 -0
- package/pipeline/scripts/_guard_rules.py +449 -1
- package/pipeline/scripts/_memory-policy.mjs +165 -0
- package/pipeline/scripts/_secret-scan.mjs +105 -0
- package/pipeline/scripts/_test-gaming.mjs +36 -0
- package/pipeline/scripts/_unified-diff.mjs +2 -2
- package/pipeline/scripts/agent-guard.py +79 -19
- package/pipeline/scripts/anonymize-findings.mjs +99 -0
- package/pipeline/scripts/answer-check.mjs +113 -0
- package/pipeline/scripts/answer-check.sh +20 -0
- package/pipeline/scripts/autopilot-publish.mjs +9 -2
- package/pipeline/scripts/capture-flush.sh +12 -2
- package/pipeline/scripts/check-read-size.sh +9 -0
- package/pipeline/scripts/code-graph-rules/android.json +8 -3
- package/pipeline/scripts/contract-server.mjs +71 -16
- package/pipeline/scripts/debt-ledger.mjs +323 -0
- package/pipeline/scripts/file-context.mjs +261 -0
- package/pipeline/scripts/file-context.sh +20 -0
- package/pipeline/scripts/gate-ledger.mjs +144 -21
- package/pipeline/scripts/graph-links.mjs +230 -0
- package/pipeline/scripts/graph-query.mjs +133 -1
- package/pipeline/scripts/graph-report.mjs +65 -0
- package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
- package/pipeline/scripts/learnings-ledger.mjs +143 -24
- package/pipeline/scripts/office-build.mjs +276 -5
- package/pipeline/scripts/office-control.mjs +70 -10
- package/pipeline/scripts/office-pair.mjs +407 -0
- package/pipeline/scripts/office-remote-client.mjs +463 -0
- package/pipeline/scripts/phase-tracker.sh +8 -0
- package/pipeline/scripts/phase0-exit-gate.mjs +40 -37
- package/pipeline/scripts/phone-devices.mjs +9 -1
- package/pipeline/scripts/plan-critique-gate.mjs +11 -2
- package/pipeline/scripts/pre-commit-check.sh +28 -2
- package/pipeline/scripts/pre-push-check.sh +6 -12
- package/pipeline/scripts/rework-base.mjs +87 -0
- package/pipeline/scripts/rework-rulings.mjs +106 -0
- package/pipeline/scripts/run-commit-check.mjs +104 -0
- package/pipeline/scripts/run-scope-guard.mjs +58 -3
- package/pipeline/scripts/run-scope-guard.sh +13 -1
- package/pipeline/scripts/scope-check-gate.mjs +213 -2
- package/pipeline/scripts/task-mirror.mjs +106 -15
- package/pipeline/scripts/task-mirror.sh +3 -2
- package/pipeline/scripts/test-stability-gate.mjs +112 -0
- package/pipeline/scripts/triage-memory.mjs +64 -37
- package/pipeline/scripts/update-check.sh +7 -1
- package/pipeline/scripts/update-issue-progress.sh +73 -8
- package/pipeline/scripts/validate-planning.mjs +172 -4
- package/pipeline/scripts/validate-reviewer.mjs +20 -0
- package/pipeline/scripts/validate-triage.mjs +149 -4
- package/pipeline/scripts/work-items.mjs +119 -1
- package/pipeline/scripts/worktree-prepare-all.mjs +200 -0
- package/pipeline/scripts/worktree-prepare.sh +40 -4
- package/pipeline/scripts/write-state.mjs +67 -24
- package/pipeline/skills/.skill-manifest.json +9 -9
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +5 -5
- package/pipeline/skills/shared/core/google-play-compliance/references/rules.yml +1 -1
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +2 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +14 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +6 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +10 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +11 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +8 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/references/patterns.md +6 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +10 -0
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +13 -0
- package/pipeline/skills/shared/external/android-security/references/patterns.md +16 -0
- package/pipeline/skills/shared/external/api-patterns/references/contract-details.md +9 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/abuse-controls.md +12 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/identity-and-access.md +13 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/request-handling.md +12 -0
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +9 -0
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +8 -0
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +10 -0
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +21 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +9 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +7 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +2 -2
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +10 -0
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +10 -1
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +22 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +17 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +22 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +17 -0
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +9 -0
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +11 -0
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +13 -0
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +9 -0
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +10 -0
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +15 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +7 -0
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +7 -0
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +15 -0
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +9 -0
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +11 -0
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +10 -0
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +13 -0
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +20 -0
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +16 -0
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +16 -0
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +14 -0
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +13 -0
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +10 -0
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +10 -0
- package/pipeline/skills/shared/external/fastapi-pro/references/app-structure.md +10 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +17 -0
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +12 -0
- package/pipeline/skills/shared/external/hig-app-icons/references/icon-workflow.md +10 -0
- package/pipeline/skills/shared/external/hig-components-content/references/content-views.md +10 -0
- package/pipeline/skills/shared/external/hig-components-layout/references/bars-and-tabs.md +11 -0
- package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +13 -0
- package/pipeline/skills/shared/external/hig-foundations/references/type-color-spacing.md +13 -0
- package/pipeline/skills/shared/external/hig-patterns/references/onboarding-and-states.md +11 -0
- package/pipeline/skills/shared/external/hig-platforms/references/ipad-layout.md +12 -0
- package/pipeline/skills/shared/external/hig-technologies/references/technologies.md +16 -0
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +9 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +18 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/large-content-and-appearance.md +9 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +9 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +7 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +13 -0
- package/pipeline/skills/shared/external/ios-design-review/references/appearance-matrix.md +9 -0
- package/pipeline/skills/shared/external/ios-design-review/references/checklist.md +15 -0
- package/pipeline/skills/shared/external/ios-haptics/references/core-haptics.md +11 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/api-map.md +11 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +15 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/poses-and-panes.md +11 -0
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +18 -0
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +19 -0
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +15 -0
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +7 -0
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +8 -0
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +12 -0
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +17 -0
- package/pipeline/skills/shared/external/ios-security/references/app-review-guidelines.md +8 -0
- package/pipeline/skills/shared/external/ios-security/references/cryptokit-advanced.md +14 -0
- package/pipeline/skills/shared/external/ios-security/references/file-storage-patterns.md +8 -0
- package/pipeline/skills/shared/external/ios-security/references/privacy-manifest.md +9 -0
- package/pipeline/skills/shared/external/ios-sf-symbols/references/effects-and-custom-symbols.md +9 -0
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +13 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/references/patterns.md +8 -0
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +21 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +9 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -0
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +16 -0
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +21 -0
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +10 -0
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +9 -0
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +12 -0
- package/pipeline/skills/shared/external/nodejs-backend-patterns/references/runtime-patterns.md +11 -0
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +11 -0
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +18 -0
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +11 -0
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +11 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +15 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +14 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +14 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +14 -0
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +14 -0
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +10 -0
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +12 -0
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +14 -0
- package/pipeline/skills/shared/external/room-database/references/patterns.md +16 -0
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +10 -0
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +10 -0
- package/pipeline/skills/shared/external/spm-build-analysis/references/build-optimization-sources.md +14 -0
- package/pipeline/skills/shared/external/spm-build-analysis/references/spm-analysis-checks.md +19 -0
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +13 -0
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +31 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +8 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +9 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +10 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +8 -0
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +24 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +8 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +14 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +12 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +10 -0
- package/pipeline/skills/shared/external/swift-concurrency-expert/references/swift-6-2-concurrency.md +8 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/actors.md +10 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/cancellation.md +7 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/interop.md +10 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/new-features.md +11 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/structured.md +9 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/testing.md +13 -0
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +7 -0
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +14 -0
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +19 -0
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +19 -0
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +18 -0
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +18 -0
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +21 -0
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +16 -0
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +9 -0
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +24 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/async-tests.md +9 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/new-features.md +11 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/writing-better-tests.md +13 -0
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +8 -0
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +17 -0
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +12 -0
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +11 -0
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +7 -0
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +13 -0
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +24 -0
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +12 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +15 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +15 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/scroll-effects.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/accessibility-patterns.md +11 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +7 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +13 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts-accessibility.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts.md +17 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/image-optimization.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/latest-apis.md +10 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/layout-best-practices.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/list-patterns.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-scenes.md +13 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-views.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-window-styling.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/performance-patterns.md +7 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/scroll-patterns.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/sheet-navigation-patterns.md +10 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/state-management.md +18 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/view-structure.md +16 -0
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +11 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +8 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +14 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +9 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +22 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/uikit-glass.md +14 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +10 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +10 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +13 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +10 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +14 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +30 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +16 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +11 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +11 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +10 -0
- package/pipeline/skills/shared/external/swiftui-performance-audit/references/code-smells.md +8 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/app-wiring.md +12 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/menu-bar.md +10 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/navigationstack.md +11 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/scroll-reveal.md +12 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/sheets.md +14 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/tabview.md +10 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +10 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +13 -0
- package/pipeline/skills/shared/external/swiftui-view-refactor/references/mv-patterns.md +12 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +8 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +8 -0
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +14 -0
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +17 -0
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +11 -0
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +11 -0
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +27 -0
- package/pipeline/skills/shared/external/xcode-build-fixer/references/build-settings-best-practices.md +9 -0
- package/pipeline/skills/shared/external/xcode-build-fixer/references/fix-patterns.md +7 -0
- package/pipeline/skills/shared/external/xcode-build-orchestrator/references/build-settings-best-practices.md +9 -0
- package/pipeline/skills/shared/external/xcode-compilation-analyzer/references/build-optimization-sources.md +14 -0
- package/pipeline/skills/shared/external/xcode-compilation-analyzer/references/code-compilation-checks.md +8 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/references/build-optimization-sources.md +14 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/references/build-settings-best-practices.md +9 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/references/project-audit-checks.md +13 -0
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* status running or not, the URL without the token, the idle
|
|
24
24
|
* limit, remote access, the paired devices the phone
|
|
25
25
|
* registry lists, and the last panel actions from
|
|
26
|
-
* office-actions.jsonl. Remote access is "
|
|
27
|
-
*
|
|
28
|
-
* prefs.global.office.remote
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* office-actions.jsonl. Remote access is "off" until
|
|
27
|
+
* office-pair pairs a phone, "configured" when
|
|
28
|
+
* prefs.global.office.remote is set but nothing is paired,
|
|
29
|
+
* and "paired" once a device is; while paired, office-on
|
|
30
|
+
* starts office-remote-client.mjs beside the server.
|
|
31
31
|
*
|
|
32
32
|
* State: one 0600 file, ~/.claude/logs/multi-agent/office/office.json, with
|
|
33
33
|
* the pid, port and token file of the server this script started. The server
|
|
@@ -58,6 +58,7 @@ import { homedir } from "node:os";
|
|
|
58
58
|
import { dirname, join } from "node:path";
|
|
59
59
|
|
|
60
60
|
import { codeDigest } from "../lib/code-digest.mjs";
|
|
61
|
+
import { phoneDir } from "./_phone-auth.mjs";
|
|
61
62
|
import { fileURLToPath } from "node:url";
|
|
62
63
|
|
|
63
64
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -72,6 +73,7 @@ const START_SKEW_MS = 120 * 1000;
|
|
|
72
73
|
const home = (env) => env.HOME || homedir();
|
|
73
74
|
const stateDir = (env) => join(home(env), ".claude", "logs", "multi-agent", "office");
|
|
74
75
|
const stateFile = (env) => join(stateDir(env), "office.json");
|
|
76
|
+
const clientFile = (env) => join(stateDir(env), "remote-client.json");
|
|
75
77
|
const actionsLog = (env) =>
|
|
76
78
|
join(home(env), ".claude", "logs", "multi-agent", "office-actions.jsonl");
|
|
77
79
|
|
|
@@ -86,16 +88,22 @@ const readJson = (p) => {
|
|
|
86
88
|
/** The panel page: next to this scripts directory, in a checkout and in the install. */
|
|
87
89
|
export const pagePath = () => join(HERE, "..", "office", "index.html");
|
|
88
90
|
|
|
89
|
-
/** Whether a
|
|
91
|
+
/** Whether a device is paired for remote use (office-pair wrote the remote config). */
|
|
92
|
+
export const remotePaired = (env = process.env) => existsSync(join(phoneDir(env), "remote.json"));
|
|
93
|
+
|
|
94
|
+
/** Whether the remote layer is configured: a paired device, or a prefs block. */
|
|
90
95
|
export function remoteLayer(env = process.env) {
|
|
96
|
+
if (remotePaired(env)) return true;
|
|
91
97
|
const prefs = readJson(join(home(env), ".claude", "multi-agent-preferences.json"));
|
|
92
98
|
const block = prefs?.global?.office?.remote;
|
|
93
|
-
|
|
94
|
-
return existsSync(join(home(env), ".claude", "office-remote"));
|
|
99
|
+
return Boolean(block && typeof block === "object" && !Array.isArray(block));
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
const remoteText = (env) =>
|
|
98
|
-
|
|
102
|
+
const remoteText = (env) => {
|
|
103
|
+
if (remotePaired(env)) return "paired: the remote client runs while the office is on";
|
|
104
|
+
if (remoteLayer(env)) return "configured, no phone paired (office-pair start)";
|
|
105
|
+
return "off: no phone paired";
|
|
106
|
+
};
|
|
99
107
|
|
|
100
108
|
/** The contract server: beside this script, else the install. */
|
|
101
109
|
export function serverScript(env = process.env) {
|
|
@@ -127,6 +135,55 @@ function psField(pid, field) {
|
|
|
127
135
|
return r.status === 0 ? r.stdout.trim() : "";
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/** The remote client this script started, if it is still running. */
|
|
139
|
+
function remoteClient(env) {
|
|
140
|
+
const c = readJson(clientFile(env));
|
|
141
|
+
if (!c || !Number.isInteger(c.pid) || !alive(c.pid)) return null;
|
|
142
|
+
return psField(c.pid, "command").includes("office-remote-client.mjs") ? c : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Start office-remote-client.mjs beside the server when a device is paired,
|
|
147
|
+
* or keep the one already running. Without a pairing nothing starts, so
|
|
148
|
+
* remote stays off by default; the client also stops by itself once the
|
|
149
|
+
* server is gone.
|
|
150
|
+
*/
|
|
151
|
+
export function startRemoteClient(env = process.env) {
|
|
152
|
+
if (!remotePaired(env)) return false;
|
|
153
|
+
if (remoteClient(env)) return true;
|
|
154
|
+
const script = join(HERE, "office-remote-client.mjs");
|
|
155
|
+
if (!existsSync(script)) return false;
|
|
156
|
+
const child = spawn(process.execPath, [script, "loop"], {
|
|
157
|
+
env,
|
|
158
|
+
detached: true,
|
|
159
|
+
stdio: "ignore",
|
|
160
|
+
});
|
|
161
|
+
child.unref();
|
|
162
|
+
mkdirSync(stateDir(env), { recursive: true, mode: 0o700 });
|
|
163
|
+
writeFileSync(
|
|
164
|
+
clientFile(env),
|
|
165
|
+
JSON.stringify({ pid: child.pid, startedAt: new Date().toISOString() }) + "\n",
|
|
166
|
+
{ mode: 0o600 },
|
|
167
|
+
);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Stop the remote client this script started. */
|
|
172
|
+
export function stopRemoteClient(env = process.env) {
|
|
173
|
+
const c = remoteClient(env);
|
|
174
|
+
let stopped = false;
|
|
175
|
+
if (c) {
|
|
176
|
+
try {
|
|
177
|
+
process.kill(c.pid, "SIGTERM");
|
|
178
|
+
stopped = true;
|
|
179
|
+
} catch {
|
|
180
|
+
stopped = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
rmSync(clientFile(env), { force: true });
|
|
184
|
+
return stopped;
|
|
185
|
+
}
|
|
186
|
+
|
|
130
187
|
/**
|
|
131
188
|
* Whether `pid` is still the server a state record names: its token file
|
|
132
189
|
* names the same pid, the process runs contract-server.mjs, and it started
|
|
@@ -313,6 +370,7 @@ export async function officeOn({ env = process.env, open = true } = {}) {
|
|
|
313
370
|
if (!s) throw new Error("the server stopped right after it started");
|
|
314
371
|
}
|
|
315
372
|
const page = `${s.url}/office`;
|
|
373
|
+
const remote = startRemoteClient(env);
|
|
316
374
|
const opened = open ? openBrowser(`${page}#code=${await panelCode(s)}`, env) : false;
|
|
317
375
|
return {
|
|
318
376
|
running: true,
|
|
@@ -321,12 +379,14 @@ export async function officeOn({ env = process.env, open = true } = {}) {
|
|
|
321
379
|
url: page,
|
|
322
380
|
pid: s.pid,
|
|
323
381
|
idleMinutes: s.idleMinutes,
|
|
382
|
+
remote,
|
|
324
383
|
opened,
|
|
325
384
|
};
|
|
326
385
|
}
|
|
327
386
|
|
|
328
387
|
/** Stop the server this script started. */
|
|
329
388
|
export function officeOff({ env = process.env } = {}) {
|
|
389
|
+
stopRemoteClient(env);
|
|
330
390
|
const s = readJson(stateFile(env));
|
|
331
391
|
let stopped = false;
|
|
332
392
|
if (isOurServer(s, s ? readJson(s.tokenFile ?? "") : null)) {
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-pair.mjs - pair a phone with this machine for the office
|
|
4
|
+
* remote layer (features/office-remote-protocol.md, Pairing).
|
|
5
|
+
*
|
|
6
|
+
* start [--relay <url>] register a pairing with the relay and show its QR
|
|
7
|
+
* code on this machine's screen
|
|
8
|
+
* wait wait for the phone's pairing message, open it and
|
|
9
|
+
* print the comparison code
|
|
10
|
+
* confirm <code> enrol the device once the code the phone shows is
|
|
11
|
+
* typed back here, and send the phone its device id
|
|
12
|
+
* in the first sealed machine-to-phone frame
|
|
13
|
+
* cancel drop the pending pairing
|
|
14
|
+
*
|
|
15
|
+
* What stays on this machine: the machine private key and the relay token
|
|
16
|
+
* live in <phone dir>/remote.json (0600); the relay only gets the SHA-256 of
|
|
17
|
+
* the token. The one-time secret and the machine public key travel in the QR
|
|
18
|
+
* link's fragment, which a browser never sends to a server, so the relay can
|
|
19
|
+
* neither open the phone's pairing message nor swap the machine key. The QR
|
|
20
|
+
* page is a 0600 file in a 0700 directory opened in the local browser; the
|
|
21
|
+
* secret is never printed. Enrolment goes through `phone-devices.mjs add`
|
|
22
|
+
* with scopes read,answer, so `launch` stays a separate local switch.
|
|
23
|
+
*
|
|
24
|
+
* Every subcommand refuses an unattended run (unattended-policy.json).
|
|
25
|
+
* Exit: 0 done, 1 refused or failed, 2 usage, 3 unattended.
|
|
26
|
+
*
|
|
27
|
+
* @module pipeline/scripts/office-pair
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
32
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
comparisonCode,
|
|
39
|
+
createMachineKeys,
|
|
40
|
+
directionKeys,
|
|
41
|
+
openPairing,
|
|
42
|
+
sealFrame,
|
|
43
|
+
} from "../lib/office-remote.mjs";
|
|
44
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
45
|
+
import { encodeQr, qrSvg } from "../lib/qr-code.mjs";
|
|
46
|
+
import { isUnattended } from "../lib/unattended.mjs";
|
|
47
|
+
import { phoneDir } from "./_phone-auth.mjs";
|
|
48
|
+
import { readConfig, writeConfig } from "./office-remote-client.mjs";
|
|
49
|
+
|
|
50
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
export const PAIRING_TTL_MS = 10 * 60_000;
|
|
52
|
+
const NAME = /^[\p{L}\p{N} ._'-]{1,64}$/u;
|
|
53
|
+
const B64U = /^[A-Za-z0-9_-]+$/;
|
|
54
|
+
|
|
55
|
+
const home = (env) => env.HOME || homedir();
|
|
56
|
+
export const pendingPath = (env = process.env) => join(phoneDir(env), "pair-pending.json");
|
|
57
|
+
const pageDir = (env) => join(home(env), ".claude", "logs", "multi-agent", "office");
|
|
58
|
+
const pagePath = (env) => join(pageDir(env), "pair.html");
|
|
59
|
+
const b64u = (b) => Buffer.from(b).toString("base64url");
|
|
60
|
+
const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
|
|
61
|
+
|
|
62
|
+
/** First 16 hex digits of SHA-256 over a public key, in groups of four (the phone shows the same). */
|
|
63
|
+
export const keyFingerprint = (publicKey) =>
|
|
64
|
+
createHash("sha256")
|
|
65
|
+
.update(Buffer.from(publicKey, "base64url"))
|
|
66
|
+
.digest("hex")
|
|
67
|
+
.slice(0, 16)
|
|
68
|
+
.replace(/(.{4})(?=.)/g, "$1 ");
|
|
69
|
+
|
|
70
|
+
class Refusal extends Error {}
|
|
71
|
+
|
|
72
|
+
function readJson(p) {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writePrivate(file, text) {
|
|
81
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
82
|
+
chmodSync(dirname(file), 0o700);
|
|
83
|
+
writeFileSync(file, text, { mode: 0o600 });
|
|
84
|
+
chmodSync(file, 0o600);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A relay URL: https, or http on the loopback; no credentials, query or fragment. */
|
|
88
|
+
export function relayOrigin(raw) {
|
|
89
|
+
let u;
|
|
90
|
+
try {
|
|
91
|
+
u = new URL(String(raw));
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Refusal("the relay URL is not a URL");
|
|
94
|
+
}
|
|
95
|
+
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
|
|
96
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback)) {
|
|
97
|
+
throw new Refusal("the relay URL must be https");
|
|
98
|
+
}
|
|
99
|
+
if (u.username || u.password || u.search || u.hash || (u.pathname && u.pathname !== "/")) {
|
|
100
|
+
throw new Refusal("the relay URL is an origin only (scheme, host, port)");
|
|
101
|
+
}
|
|
102
|
+
return u.origin;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function relayFromPrefs(env) {
|
|
106
|
+
const prefs = readJson(join(home(env), ".claude", "multi-agent-preferences.json"));
|
|
107
|
+
return prefs?.global?.office?.remote?.relayUrl ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The link the QR code carries; everything after `#` stays on the phone. */
|
|
111
|
+
export function pairingLink({ relayUrl, pairingId, secret, machinePublic }) {
|
|
112
|
+
return `${relayUrl}/office/pair#p=${pairingId}&s=${secret}&k=${machinePublic}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pageHtml(svg) {
|
|
116
|
+
return (
|
|
117
|
+
"<!doctype html><html><head><meta charset=utf-8>" +
|
|
118
|
+
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'">` +
|
|
119
|
+
"<title>Office pairing</title></head><body>" +
|
|
120
|
+
"<p>Scan with the phone, signed in to your site. The code expires in 10 minutes.</p>" +
|
|
121
|
+
`${svg}</body></html>`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function openPage(file, env) {
|
|
126
|
+
const bin = env.MA_OFFICE_OPENER || "open";
|
|
127
|
+
const r = spawnSync(bin, [`file://${file}`], { stdio: "ignore", timeout: 15000 });
|
|
128
|
+
return r.status === 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function relayCall(fetchImpl, url, init) {
|
|
132
|
+
const res = await fetchImpl(url, { ...init, signal: AbortSignal.timeout(30_000) });
|
|
133
|
+
const text = await res.text();
|
|
134
|
+
let body;
|
|
135
|
+
try {
|
|
136
|
+
body = text ? JSON.parse(text) : null;
|
|
137
|
+
} catch {
|
|
138
|
+
body = null;
|
|
139
|
+
}
|
|
140
|
+
return { status: res.status, ok: res.ok, body };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register a pairing with the relay and show its QR code.
|
|
145
|
+
*
|
|
146
|
+
* @returns {Promise<{pairingId: string, expiresAt: string, page: string, opened: boolean}>}
|
|
147
|
+
*/
|
|
148
|
+
export async function start({ relay, env = process.env, fetchImpl = fetch, now = Date.now() }) {
|
|
149
|
+
const relayUrl = relayOrigin(relay ?? relayFromPrefs(env) ?? "");
|
|
150
|
+
const existing = readConfig(env);
|
|
151
|
+
const machine =
|
|
152
|
+
existing?.machinePrivatePem && existing?.machinePublic
|
|
153
|
+
? { privatePem: existing.machinePrivatePem, publicKey: existing.machinePublic }
|
|
154
|
+
: createMachineKeys();
|
|
155
|
+
const relayToken =
|
|
156
|
+
existing?.relayToken && existing.relayUrl === relayUrl
|
|
157
|
+
? existing.relayToken
|
|
158
|
+
: b64u(randomBytes(32));
|
|
159
|
+
const pairingId = b64u(randomBytes(16));
|
|
160
|
+
const secret = b64u(randomBytes(16));
|
|
161
|
+
const expiresAt = new Date(now + PAIRING_TTL_MS).toISOString();
|
|
162
|
+
const r = await relayCall(fetchImpl, `${relayUrl}/api/office/relay/pairings`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "content-type": "application/json" },
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
pairingId,
|
|
167
|
+
machinePublic: machine.publicKey,
|
|
168
|
+
tokenHash: sha256hex(relayToken),
|
|
169
|
+
expiresAt,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
if (!r.ok) throw new Refusal(`the relay refused the pairing (${r.status})`);
|
|
173
|
+
writeConfig(
|
|
174
|
+
{
|
|
175
|
+
...(existing ?? {}),
|
|
176
|
+
relayUrl,
|
|
177
|
+
relayToken,
|
|
178
|
+
pairingId: existing?.pairingId ?? null,
|
|
179
|
+
machinePrivatePem: machine.privatePem,
|
|
180
|
+
machinePublic: machine.publicKey,
|
|
181
|
+
devices: existing?.devices ?? {},
|
|
182
|
+
},
|
|
183
|
+
env,
|
|
184
|
+
);
|
|
185
|
+
writePrivate(
|
|
186
|
+
pendingPath(env),
|
|
187
|
+
JSON.stringify({ relayUrl, pairingId, secret, expiresAt, relayToken }) + "\n",
|
|
188
|
+
);
|
|
189
|
+
const link = pairingLink({ relayUrl, pairingId, secret, machinePublic: machine.publicKey });
|
|
190
|
+
writePrivate(pagePath(env), pageHtml(qrSvg(encodeQr(link, { ecc: "M" }))));
|
|
191
|
+
return { pairingId, expiresAt, page: pagePath(env), opened: openPage(pagePath(env), env) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function pending(env, now) {
|
|
195
|
+
const p = readJson(pendingPath(env));
|
|
196
|
+
if (!p?.pairingId) throw new Refusal("no pairing is pending; run office-pair start");
|
|
197
|
+
if (Date.parse(p.expiresAt) <= now) {
|
|
198
|
+
dropPending(env);
|
|
199
|
+
throw new Refusal("the pairing expired; run office-pair start again");
|
|
200
|
+
}
|
|
201
|
+
return p;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dropPending(env) {
|
|
205
|
+
rmSync(pendingPath(env), { force: true });
|
|
206
|
+
rmSync(pagePath(env), { force: true });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Check what the phone sent before anything of it is kept. */
|
|
210
|
+
export function checkPairingMessage(value, relayUrl) {
|
|
211
|
+
const v = value ?? {};
|
|
212
|
+
if (typeof v.deviceName !== "string" || !NAME.test(v.deviceName))
|
|
213
|
+
throw new Refusal("the device name is not 1 to 64 letters, digits, spaces or ._'-");
|
|
214
|
+
if (typeof v.devicePublicKey !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(v.devicePublicKey))
|
|
215
|
+
throw new Refusal("the device key is not a base64url Ed25519 public key");
|
|
216
|
+
const pk = v.passkey ?? {};
|
|
217
|
+
if (typeof pk.credential !== "string" || !B64U.test(pk.credential) || pk.credential.length > 400)
|
|
218
|
+
throw new Refusal("the passkey public key is missing");
|
|
219
|
+
if (typeof pk.credentialId !== "string" || !B64U.test(pk.credentialId))
|
|
220
|
+
throw new Refusal("the passkey credential id is missing");
|
|
221
|
+
if (pk.origin !== relayUrl) throw new Refusal("the passkey origin is not the relay's");
|
|
222
|
+
if (pk.rpId !== new URL(relayUrl).hostname)
|
|
223
|
+
throw new Refusal("the passkey rp id is not the relay's host");
|
|
224
|
+
return {
|
|
225
|
+
deviceName: v.deviceName,
|
|
226
|
+
devicePublicKey: v.devicePublicKey,
|
|
227
|
+
passkey: {
|
|
228
|
+
credential: pk.credential,
|
|
229
|
+
credentialId: pk.credentialId,
|
|
230
|
+
rpId: pk.rpId,
|
|
231
|
+
origin: pk.origin,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Wait for the phone's pairing message, open it and keep it with the pending
|
|
238
|
+
* pairing. Prints the comparison code, never a key.
|
|
239
|
+
*/
|
|
240
|
+
export async function wait({
|
|
241
|
+
env = process.env,
|
|
242
|
+
fetchImpl = fetch,
|
|
243
|
+
now = Date.now,
|
|
244
|
+
pollMs = 2000,
|
|
245
|
+
}) {
|
|
246
|
+
const p = pending(env, now());
|
|
247
|
+
const config = readConfig(env);
|
|
248
|
+
while (now() < Date.parse(p.expiresAt)) {
|
|
249
|
+
const r = await relayCall(
|
|
250
|
+
fetchImpl,
|
|
251
|
+
`${p.relayUrl}/api/office/relay/pairings/${encodeURIComponent(p.pairingId)}`,
|
|
252
|
+
{ headers: { Authorization: `Bearer ${p.relayToken}` } },
|
|
253
|
+
);
|
|
254
|
+
if (r.ok && r.body?.envelope) {
|
|
255
|
+
const { sessionPublic, sealed } = r.body.envelope;
|
|
256
|
+
if (typeof sessionPublic !== "string" || !B64U.test(sessionPublic))
|
|
257
|
+
throw new Refusal("the phone sent no session key");
|
|
258
|
+
const opened = openPairing({
|
|
259
|
+
privatePem: config.machinePrivatePem,
|
|
260
|
+
peerPublic: sessionPublic,
|
|
261
|
+
secret: p.secret,
|
|
262
|
+
pairingId: p.pairingId,
|
|
263
|
+
sealed,
|
|
264
|
+
});
|
|
265
|
+
if (!opened.ok) throw new Refusal("the pairing message did not open with this pairing");
|
|
266
|
+
const message = checkPairingMessage(opened.value, p.relayUrl);
|
|
267
|
+
writePrivate(pendingPath(env), JSON.stringify({ ...p, sessionPublic, message }) + "\n");
|
|
268
|
+
return {
|
|
269
|
+
deviceName: message.deviceName,
|
|
270
|
+
code: comparisonCode(config.machinePublic, sessionPublic),
|
|
271
|
+
machineKey: keyFingerprint(config.machinePublic),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (!r.ok && r.status !== 404) throw new Refusal(`the relay answered ${r.status}`);
|
|
275
|
+
await new Promise((res) => setTimeout(res, pollMs));
|
|
276
|
+
}
|
|
277
|
+
dropPending(env);
|
|
278
|
+
throw new Refusal("the pairing expired before the phone answered");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function enrol(message, env) {
|
|
282
|
+
const r = spawnSync(
|
|
283
|
+
process.execPath,
|
|
284
|
+
[
|
|
285
|
+
join(HERE, "phone-devices.mjs"),
|
|
286
|
+
"add",
|
|
287
|
+
"--name",
|
|
288
|
+
message.deviceName,
|
|
289
|
+
"--public-key",
|
|
290
|
+
message.devicePublicKey,
|
|
291
|
+
"--scopes",
|
|
292
|
+
"read,answer",
|
|
293
|
+
],
|
|
294
|
+
{ env, encoding: "utf8", timeout: 30_000 },
|
|
295
|
+
);
|
|
296
|
+
if (r.status !== 0) {
|
|
297
|
+
throw new Refusal(`phone-devices refused the device: ${(r.stderr || "").trim()}`);
|
|
298
|
+
}
|
|
299
|
+
return JSON.parse(r.stdout);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Enrol the device once the user typed the code the phone shows.
|
|
304
|
+
*
|
|
305
|
+
* @returns {Promise<{id: string, name: string, scopes: string[]}>}
|
|
306
|
+
*/
|
|
307
|
+
export async function confirm({ code, env = process.env, fetchImpl = fetch, now = Date.now() }) {
|
|
308
|
+
const p = pending(env, now);
|
|
309
|
+
if (!p.message || !p.sessionPublic) throw new Refusal("run office-pair wait first");
|
|
310
|
+
const config = readConfig(env);
|
|
311
|
+
const want = Buffer.from(comparisonCode(config.machinePublic, p.sessionPublic));
|
|
312
|
+
const got = Buffer.from(String(code ?? "").trim());
|
|
313
|
+
if (got.length !== want.length || !timingSafeEqual(got, want)) {
|
|
314
|
+
dropPending(env);
|
|
315
|
+
throw new Refusal("the code does not match; the pairing was dropped, start again");
|
|
316
|
+
}
|
|
317
|
+
const device = enrol(p.message, env);
|
|
318
|
+
const keys = directionKeys({
|
|
319
|
+
privatePem: config.machinePrivatePem,
|
|
320
|
+
peerPublic: p.sessionPublic,
|
|
321
|
+
pairingId: p.pairingId,
|
|
322
|
+
role: "machine",
|
|
323
|
+
});
|
|
324
|
+
const welcome = sealFrame(keys.send, 1, "machine-to-phone", {
|
|
325
|
+
deviceId: device.id,
|
|
326
|
+
scopes: device.scopes,
|
|
327
|
+
});
|
|
328
|
+
const r = await relayCall(
|
|
329
|
+
fetchImpl,
|
|
330
|
+
`${p.relayUrl}/api/office/relay/pairings/${encodeURIComponent(p.pairingId)}/confirm`,
|
|
331
|
+
{
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { Authorization: `Bearer ${p.relayToken}`, "content-type": "application/json" },
|
|
334
|
+
body: JSON.stringify({ welcome }),
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
if (!r.ok) {
|
|
338
|
+
spawnSync(process.execPath, [join(HERE, "phone-devices.mjs"), "revoke", device.id], {
|
|
339
|
+
env,
|
|
340
|
+
stdio: "ignore",
|
|
341
|
+
timeout: 30_000,
|
|
342
|
+
});
|
|
343
|
+
dropPending(env);
|
|
344
|
+
throw new Refusal(
|
|
345
|
+
`the relay refused the confirmation (${r.status}); the device was revoked again, start over`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
writeConfig(
|
|
349
|
+
{
|
|
350
|
+
...config,
|
|
351
|
+
pairingId: p.pairingId,
|
|
352
|
+
devices: {
|
|
353
|
+
[device.id]: {
|
|
354
|
+
id: device.id,
|
|
355
|
+
sessionPublic: p.sessionPublic,
|
|
356
|
+
passkey: { ...p.message.passkey, signCount: 0 },
|
|
357
|
+
recvCounter: 0,
|
|
358
|
+
sendCounter: 1,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
env,
|
|
363
|
+
);
|
|
364
|
+
dropPending(env);
|
|
365
|
+
return { id: device.id, name: device.name, scopes: device.scopes };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Drop the pending pairing and its QR page. */
|
|
369
|
+
export function cancel({ env = process.env } = {}) {
|
|
370
|
+
const had = existsSync(pendingPath(env));
|
|
371
|
+
dropPending(env);
|
|
372
|
+
return { cancelled: had };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function main(argv) {
|
|
376
|
+
const [cmd, ...rest] = argv;
|
|
377
|
+
if (!["start", "wait", "confirm", "cancel"].includes(cmd)) {
|
|
378
|
+
process.stderr.write(
|
|
379
|
+
"usage: office-pair.mjs start [--relay <url>] | wait | confirm <code> | cancel\n",
|
|
380
|
+
);
|
|
381
|
+
return 2;
|
|
382
|
+
}
|
|
383
|
+
if (isUnattended()) {
|
|
384
|
+
process.stderr.write("office-pair: an unattended run may not pair a device\n");
|
|
385
|
+
return 3;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
let out;
|
|
389
|
+
if (cmd === "start") {
|
|
390
|
+
const i = rest.indexOf("--relay");
|
|
391
|
+
out = await start({ relay: i === -1 ? undefined : rest[i + 1] });
|
|
392
|
+
} else if (cmd === "wait") out = await wait({});
|
|
393
|
+
else if (cmd === "confirm") out = await confirm({ code: rest[0] });
|
|
394
|
+
else out = cancel();
|
|
395
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
396
|
+
return 0;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
process.stderr.write(`office-pair: ${err.message}\n`);
|
|
399
|
+
return 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (invokedDirectly(import.meta.url)) {
|
|
404
|
+
main(process.argv.slice(2)).then((code) => {
|
|
405
|
+
process.exitCode = code;
|
|
406
|
+
});
|
|
407
|
+
}
|