diff --git a/plugins/agentbridge/server/bridge-server.js b/plugins/agentbridge/server/bridge-server.js index 7bbd114..fdfe01b 100755 --- a/plugins/agentbridge/server/bridge-server.js +++ b/plugins/agentbridge/server/bridge-server.js @@ -45,7 +45,7 @@ var __export = (target, all) => { }); }; -// ../../../node_modules/ajv/dist/compile/codegen/code.js +// node_modules/ajv/dist/compile/codegen/code.js var require_code = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.regexpCode = exports.getEsmExportName = exports.getProperty = exports.safeStringify = exports.stringify = exports.strConcat = exports.addCodeArg = exports.str = exports._ = exports.nil = exports._Code = exports.Name = exports.IDENTIFIER = exports._CodeOrName = undefined; @@ -199,7 +199,7 @@ var require_code = __commonJS((exports) => { exports.regexpCode = regexpCode; }); -// ../../../node_modules/ajv/dist/compile/codegen/scope.js +// node_modules/ajv/dist/compile/codegen/scope.js var require_scope = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.ValueScope = exports.ValueScopeName = exports.Scope = exports.varKinds = exports.UsedValueState = undefined; @@ -345,7 +345,7 @@ var require_scope = __commonJS((exports) => { exports.ValueScope = ValueScope; }); -// ../../../node_modules/ajv/dist/compile/codegen/index.js +// node_modules/ajv/dist/compile/codegen/index.js var require_codegen = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.or = exports.and = exports.not = exports.CodeGen = exports.operators = exports.varKinds = exports.ValueScopeName = exports.ValueScope = exports.Scope = exports.Name = exports.regexpCode = exports.stringify = exports.getProperty = exports.nil = exports.strConcat = exports.str = exports._ = undefined; @@ -1055,7 +1055,7 @@ var require_codegen = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/compile/util.js +// node_modules/ajv/dist/compile/util.js var require_util = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.checkStrictMode = exports.getErrorPath = exports.Type = exports.useFunc = exports.setEvaluated = exports.evaluatedPropsToName = exports.mergeEvaluated = exports.eachItem = exports.unescapeJsonPointer = exports.escapeJsonPointer = exports.escapeFragment = exports.unescapeFragment = exports.schemaRefOrVal = exports.schemaHasRulesButRef = exports.schemaHasRules = exports.checkUnknownRules = exports.alwaysValidSchema = exports.toHash = undefined; @@ -1219,7 +1219,7 @@ var require_util = __commonJS((exports) => { exports.checkStrictMode = checkStrictMode; }); -// ../../../node_modules/ajv/dist/compile/names.js +// node_modules/ajv/dist/compile/names.js var require_names = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -1244,7 +1244,7 @@ var require_names = __commonJS((exports) => { exports.default = names; }); -// ../../../node_modules/ajv/dist/compile/errors.js +// node_modules/ajv/dist/compile/errors.js var require_errors = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.extendErrors = exports.resetErrorsCount = exports.reportExtraError = exports.reportError = exports.keyword$DataError = exports.keywordError = undefined; @@ -1362,7 +1362,7 @@ var require_errors = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/compile/validate/boolSchema.js +// node_modules/ajv/dist/compile/validate/boolSchema.js var require_boolSchema = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.boolOrEmptySchema = exports.topBoolOrEmptySchema = undefined; @@ -1410,7 +1410,7 @@ var require_boolSchema = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/compile/rules.js +// node_modules/ajv/dist/compile/rules.js var require_rules = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.getRules = exports.isJSONType = undefined; @@ -1438,7 +1438,7 @@ var require_rules = __commonJS((exports) => { exports.getRules = getRules; }); -// ../../../node_modules/ajv/dist/compile/validate/applicability.js +// node_modules/ajv/dist/compile/validate/applicability.js var require_applicability = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.shouldUseRule = exports.shouldUseGroup = exports.schemaHasRulesForType = undefined; @@ -1458,7 +1458,7 @@ var require_applicability = __commonJS((exports) => { exports.shouldUseRule = shouldUseRule; }); -// ../../../node_modules/ajv/dist/compile/validate/dataType.js +// node_modules/ajv/dist/compile/validate/dataType.js var require_dataType = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.reportTypeError = exports.checkDataTypes = exports.checkDataType = exports.coerceAndCheckDataType = exports.getJSONTypes = exports.getSchemaTypes = exports.DataType = undefined; @@ -1639,7 +1639,7 @@ var require_dataType = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/compile/validate/defaults.js +// node_modules/ajv/dist/compile/validate/defaults.js var require_defaults = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.assignDefaults = undefined; @@ -1673,7 +1673,7 @@ var require_defaults = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/vocabularies/code.js +// node_modules/ajv/dist/vocabularies/code.js var require_code2 = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.validateUnion = exports.validateArray = exports.usePattern = exports.callValidateCode = exports.schemaProperties = exports.allSchemaProperties = exports.noPropertyInData = exports.propertyInData = exports.isOwnProperty = exports.hasPropFunc = exports.reportMissingProp = exports.checkMissingProp = exports.checkReportMissingProp = undefined; @@ -1802,7 +1802,7 @@ var require_code2 = __commonJS((exports) => { exports.validateUnion = validateUnion; }); -// ../../../node_modules/ajv/dist/compile/validate/keyword.js +// node_modules/ajv/dist/compile/validate/keyword.js var require_keyword = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.validateKeywordUsage = exports.validSchemaType = exports.funcKeywordCode = exports.macroKeywordCode = undefined; @@ -1917,7 +1917,7 @@ var require_keyword = __commonJS((exports) => { exports.validateKeywordUsage = validateKeywordUsage; }); -// ../../../node_modules/ajv/dist/compile/validate/subschema.js +// node_modules/ajv/dist/compile/validate/subschema.js var require_subschema = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.extendSubschemaMode = exports.extendSubschemaData = exports.getSubschema = undefined; @@ -1997,7 +1997,7 @@ var require_subschema = __commonJS((exports) => { exports.extendSubschemaMode = extendSubschemaMode; }); -// ../../../node_modules/fast-deep-equal/index.js +// node_modules/fast-deep-equal/index.js var require_fast_deep_equal = __commonJS((exports, module) => { module.exports = function equal(a, b) { if (a === b) @@ -2039,7 +2039,7 @@ var require_fast_deep_equal = __commonJS((exports, module) => { }; }); -// ../../../node_modules/json-schema-traverse/index.js +// node_modules/json-schema-traverse/index.js var require_json_schema_traverse = __commonJS((exports, module) => { var traverse = module.exports = function(schema, opts, cb) { if (typeof opts == "function") { @@ -2122,7 +2122,7 @@ var require_json_schema_traverse = __commonJS((exports, module) => { } }); -// ../../../node_modules/ajv/dist/compile/resolve.js +// node_modules/ajv/dist/compile/resolve.js var require_resolve = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.getSchemaRefs = exports.resolveUrl = exports.normalizeId = exports._getFullPath = exports.getFullPath = exports.inlineRef = undefined; @@ -2275,7 +2275,7 @@ var require_resolve = __commonJS((exports) => { exports.getSchemaRefs = getSchemaRefs; }); -// ../../../node_modules/ajv/dist/compile/validate/index.js +// node_modules/ajv/dist/compile/validate/index.js var require_validate = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.getData = exports.KeywordCxt = exports.validateFunctionCode = undefined; @@ -2780,7 +2780,7 @@ var require_validate = __commonJS((exports) => { exports.getData = getData; }); -// ../../../node_modules/ajv/dist/runtime/validation_error.js +// node_modules/ajv/dist/runtime/validation_error.js var require_validation_error = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); @@ -2794,7 +2794,7 @@ var require_validation_error = __commonJS((exports) => { exports.default = ValidationError; }); -// ../../../node_modules/ajv/dist/compile/ref_error.js +// node_modules/ajv/dist/compile/ref_error.js var require_ref_error = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var resolve_1 = require_resolve(); @@ -2809,7 +2809,7 @@ var require_ref_error = __commonJS((exports) => { exports.default = MissingRefError; }); -// ../../../node_modules/ajv/dist/compile/index.js +// node_modules/ajv/dist/compile/index.js var require_compile = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveSchema = exports.getCompilingSchema = exports.resolveRef = exports.compileSchema = exports.SchemaEnv = undefined; @@ -3030,7 +3030,7 @@ var require_compile = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/refs/data.json +// node_modules/ajv/dist/refs/data.json var require_data = __commonJS((exports, module) => { module.exports = { $id: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", @@ -3047,7 +3047,7 @@ var require_data = __commonJS((exports, module) => { }; }); -// ../../../node_modules/fast-uri/lib/utils.js +// node_modules/fast-uri/lib/utils.js var require_utils = __commonJS((exports, module) => { var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu); var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u); @@ -3302,7 +3302,7 @@ var require_utils = __commonJS((exports, module) => { }; }); -// ../../../node_modules/fast-uri/lib/schemes.js +// node_modules/fast-uri/lib/schemes.js var require_schemes = __commonJS((exports, module) => { var { isUUID } = require_utils(); var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu; @@ -3476,7 +3476,7 @@ var require_schemes = __commonJS((exports, module) => { }; }); -// ../../../node_modules/fast-uri/index.js +// node_modules/fast-uri/index.js var require_fast_uri = __commonJS((exports, module) => { var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils(); var { SCHEMES, getSchemeHandler } = require_schemes(); @@ -3727,7 +3727,7 @@ var require_fast_uri = __commonJS((exports, module) => { module.exports.fastUri = fastUri; }); -// ../../../node_modules/ajv/dist/runtime/uri.js +// node_modules/ajv/dist/runtime/uri.js var require_uri = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var uri = require_fast_uri(); @@ -3735,7 +3735,7 @@ var require_uri = __commonJS((exports) => { exports.default = uri; }); -// ../../../node_modules/ajv/dist/core.js +// node_modules/ajv/dist/core.js var require_core = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = undefined; @@ -4328,7 +4328,7 @@ var require_core = __commonJS((exports) => { } }); -// ../../../node_modules/ajv/dist/vocabularies/core/id.js +// node_modules/ajv/dist/vocabularies/core/id.js var require_id = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var def = { @@ -4340,7 +4340,7 @@ var require_id = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/core/ref.js +// node_modules/ajv/dist/vocabularies/core/ref.js var require_ref = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.callRef = exports.getValidate = undefined; @@ -4459,7 +4459,7 @@ var require_ref = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/core/index.js +// node_modules/ajv/dist/vocabularies/core/index.js var require_core2 = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var id_1 = require_id(); @@ -4477,7 +4477,7 @@ var require_core2 = __commonJS((exports) => { exports.default = core2; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/limitNumber.js +// node_modules/ajv/dist/vocabularies/validation/limitNumber.js var require_limitNumber = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4506,7 +4506,7 @@ var require_limitNumber = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/multipleOf.js +// node_modules/ajv/dist/vocabularies/validation/multipleOf.js var require_multipleOf = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4531,7 +4531,7 @@ var require_multipleOf = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/runtime/ucs2length.js +// node_modules/ajv/dist/runtime/ucs2length.js var require_ucs2length = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); function ucs2length(str) { @@ -4554,7 +4554,7 @@ var require_ucs2length = __commonJS((exports) => { ucs2length.code = 'require("ajv/dist/runtime/ucs2length").default'; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/limitLength.js +// node_modules/ajv/dist/vocabularies/validation/limitLength.js var require_limitLength = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4583,7 +4583,7 @@ var require_limitLength = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/pattern.js +// node_modules/ajv/dist/vocabularies/validation/pattern.js var require_pattern = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var code_1 = require_code2(); @@ -4617,7 +4617,7 @@ var require_pattern = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/limitProperties.js +// node_modules/ajv/dist/vocabularies/validation/limitProperties.js var require_limitProperties = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4643,7 +4643,7 @@ var require_limitProperties = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/required.js +// node_modules/ajv/dist/vocabularies/validation/required.js var require_required = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var code_1 = require_code2(); @@ -4722,7 +4722,7 @@ var require_required = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/limitItems.js +// node_modules/ajv/dist/vocabularies/validation/limitItems.js var require_limitItems = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4748,7 +4748,7 @@ var require_limitItems = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/runtime/equal.js +// node_modules/ajv/dist/runtime/equal.js var require_equal = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var equal = require_fast_deep_equal(); @@ -4756,7 +4756,7 @@ var require_equal = __commonJS((exports) => { exports.default = equal; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/uniqueItems.js +// node_modules/ajv/dist/vocabularies/validation/uniqueItems.js var require_uniqueItems = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var dataType_1 = require_dataType(); @@ -4820,7 +4820,7 @@ var require_uniqueItems = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/const.js +// node_modules/ajv/dist/vocabularies/validation/const.js var require_const = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4846,7 +4846,7 @@ var require_const = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/enum.js +// node_modules/ajv/dist/vocabularies/validation/enum.js var require_enum = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -4892,7 +4892,7 @@ var require_enum = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/validation/index.js +// node_modules/ajv/dist/vocabularies/validation/index.js var require_validation = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var limitNumber_1 = require_limitNumber(); @@ -4922,7 +4922,7 @@ var require_validation = __commonJS((exports) => { exports.default = validation; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/additionalItems.js +// node_modules/ajv/dist/vocabularies/applicator/additionalItems.js var require_additionalItems = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.validateAdditionalItems = undefined; @@ -4972,7 +4972,7 @@ var require_additionalItems = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/items.js +// node_modules/ajv/dist/vocabularies/applicator/items.js var require_items = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.validateTuple = undefined; @@ -5026,7 +5026,7 @@ var require_items = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/prefixItems.js +// node_modules/ajv/dist/vocabularies/applicator/prefixItems.js var require_prefixItems = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var items_1 = require_items(); @@ -5040,7 +5040,7 @@ var require_prefixItems = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/items2020.js +// node_modules/ajv/dist/vocabularies/applicator/items2020.js var require_items2020 = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -5072,7 +5072,7 @@ var require_items2020 = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/contains.js +// node_modules/ajv/dist/vocabularies/applicator/contains.js var require_contains = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -5163,7 +5163,7 @@ var require_contains = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/dependencies.js +// node_modules/ajv/dist/vocabularies/applicator/dependencies.js var require_dependencies = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.validateSchemaDeps = exports.validatePropertyDeps = exports.error = undefined; @@ -5248,7 +5248,7 @@ var require_dependencies = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/propertyNames.js +// node_modules/ajv/dist/vocabularies/applicator/propertyNames.js var require_propertyNames = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -5288,7 +5288,7 @@ var require_propertyNames = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js +// node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js var require_additionalProperties = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var code_1 = require_code2(); @@ -5391,7 +5391,7 @@ var require_additionalProperties = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/properties.js +// node_modules/ajv/dist/vocabularies/applicator/properties.js var require_properties = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var validate_1 = require_validate(); @@ -5446,7 +5446,7 @@ var require_properties = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/patternProperties.js +// node_modules/ajv/dist/vocabularies/applicator/patternProperties.js var require_patternProperties = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var code_1 = require_code2(); @@ -5517,7 +5517,7 @@ var require_patternProperties = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/not.js +// node_modules/ajv/dist/vocabularies/applicator/not.js var require_not = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var util_1 = require_util(); @@ -5545,7 +5545,7 @@ var require_not = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/anyOf.js +// node_modules/ajv/dist/vocabularies/applicator/anyOf.js var require_anyOf = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var code_1 = require_code2(); @@ -5559,7 +5559,7 @@ var require_anyOf = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/oneOf.js +// node_modules/ajv/dist/vocabularies/applicator/oneOf.js var require_oneOf = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -5614,7 +5614,7 @@ var require_oneOf = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/allOf.js +// node_modules/ajv/dist/vocabularies/applicator/allOf.js var require_allOf = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var util_1 = require_util(); @@ -5638,7 +5638,7 @@ var require_allOf = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/if.js +// node_modules/ajv/dist/vocabularies/applicator/if.js var require_if = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -5704,7 +5704,7 @@ var require_if = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/thenElse.js +// node_modules/ajv/dist/vocabularies/applicator/thenElse.js var require_thenElse = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var util_1 = require_util(); @@ -5719,7 +5719,7 @@ var require_thenElse = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/applicator/index.js +// node_modules/ajv/dist/vocabularies/applicator/index.js var require_applicator = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var additionalItems_1 = require_additionalItems(); @@ -5762,7 +5762,7 @@ var require_applicator = __commonJS((exports) => { exports.default = getApplicator; }); -// ../../../node_modules/ajv/dist/vocabularies/format/format.js +// node_modules/ajv/dist/vocabularies/format/format.js var require_format = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -5849,7 +5849,7 @@ var require_format = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/vocabularies/format/index.js +// node_modules/ajv/dist/vocabularies/format/index.js var require_format2 = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var format_1 = require_format(); @@ -5857,7 +5857,7 @@ var require_format2 = __commonJS((exports) => { exports.default = format; }); -// ../../../node_modules/ajv/dist/vocabularies/metadata.js +// node_modules/ajv/dist/vocabularies/metadata.js var require_metadata = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.contentVocabulary = exports.metadataVocabulary = undefined; @@ -5877,7 +5877,7 @@ var require_metadata = __commonJS((exports) => { ]; }); -// ../../../node_modules/ajv/dist/vocabularies/draft7.js +// node_modules/ajv/dist/vocabularies/draft7.js var require_draft7 = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var core_1 = require_core2(); @@ -5896,7 +5896,7 @@ var require_draft7 = __commonJS((exports) => { exports.default = draft7Vocabularies; }); -// ../../../node_modules/ajv/dist/vocabularies/discriminator/types.js +// node_modules/ajv/dist/vocabularies/discriminator/types.js var require_types = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.DiscrError = undefined; @@ -5907,7 +5907,7 @@ var require_types = __commonJS((exports) => { })(DiscrError || (exports.DiscrError = DiscrError = {})); }); -// ../../../node_modules/ajv/dist/vocabularies/discriminator/index.js +// node_modules/ajv/dist/vocabularies/discriminator/index.js var require_discriminator = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); var codegen_1 = require_codegen(); @@ -6009,7 +6009,7 @@ var require_discriminator = __commonJS((exports) => { exports.default = def; }); -// ../../../node_modules/ajv/dist/refs/json-schema-draft-07.json +// node_modules/ajv/dist/refs/json-schema-draft-07.json var require_json_schema_draft_07 = __commonJS((exports, module) => { module.exports = { $schema: "http://json-schema.org/draft-07/schema#", @@ -6164,7 +6164,7 @@ var require_json_schema_draft_07 = __commonJS((exports, module) => { }; }); -// ../../../node_modules/ajv/dist/ajv.js +// node_modules/ajv/dist/ajv.js var require_ajv = __commonJS((exports, module) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.MissingRefError = exports.ValidationError = exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = exports.Ajv = undefined; @@ -6232,7 +6232,7 @@ var require_ajv = __commonJS((exports, module) => { } }); }); -// ../../../node_modules/ajv-formats/dist/formats.js +// node_modules/ajv-formats/dist/formats.js var require_formats = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.formatNames = exports.fastFormats = exports.fullFormats = undefined; @@ -6409,7 +6409,7 @@ var require_formats = __commonJS((exports) => { } }); -// ../../../node_modules/ajv-formats/dist/limit.js +// node_modules/ajv-formats/dist/limit.js var require_limit = __commonJS((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.formatLimitDefinition = undefined; @@ -6478,7 +6478,7 @@ var require_limit = __commonJS((exports) => { exports.default = formatLimitPlugin; }); -// ../../../node_modules/ajv-formats/dist/index.js +// node_modules/ajv-formats/dist/index.js var require_dist = __commonJS((exports, module) => { Object.defineProperty(exports, "__esModule", { value: true }); var formats_1 = require_formats(); @@ -6520,7 +6520,7 @@ var require_dist = __commonJS((exports, module) => { // src/bridge.ts import { existsSync as existsSync7 } from "fs"; -// ../../../node_modules/zod/v4/core/core.js +// node_modules/zod/v4/core/core.js var NEVER = Object.freeze({ status: "aborted" }); @@ -6596,7 +6596,7 @@ function config(newConfig) { Object.assign(globalConfig, newConfig); return globalConfig; } -// ../../../node_modules/zod/v4/core/util.js +// node_modules/zod/v4/core/util.js var exports_util = {}; __export(exports_util, { unwrapMessage: () => unwrapMessage, @@ -7270,7 +7270,7 @@ class Class { constructor(..._args) {} } -// ../../../node_modules/zod/v4/core/errors.js +// node_modules/zod/v4/core/errors.js var initializer = (inst, def) => { inst.name = "$ZodError"; Object.defineProperty(inst, "_zod", { @@ -7336,7 +7336,7 @@ function formatError(error, mapper = (issue2) => issue2.message) { return fieldErrors; } -// ../../../node_modules/zod/v4/core/parse.js +// node_modules/zod/v4/core/parse.js var _parse = (_Err) => (schema, value, _ctx, _params) => { const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false }; const result = schema._zod.run({ value, issues: [] }, ctx); @@ -7413,7 +7413,7 @@ var _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => { var _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => { return _safeParseAsync(_Err)(schema, value, _ctx); }; -// ../../../node_modules/zod/v4/core/regexes.js +// node_modules/zod/v4/core/regexes.js var cuid = /^[cC][^\s-]{8,}$/; var cuid2 = /^[0-9a-z]+$/; var ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/; @@ -7470,7 +7470,7 @@ var _null = /^null$/i; var lowercase = /^[^A-Z]*$/; var uppercase = /^[^a-z]*$/; -// ../../../node_modules/zod/v4/core/checks.js +// node_modules/zod/v4/core/checks.js var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => { var _a; inst._zod ?? (inst._zod = {}); @@ -7859,7 +7859,7 @@ var $ZodCheckOverwrite = /* @__PURE__ */ $constructor("$ZodCheckOverwrite", (ins }; }); -// ../../../node_modules/zod/v4/core/doc.js +// node_modules/zod/v4/core/doc.js class Doc { constructor(args = []) { this.content = []; @@ -7897,14 +7897,14 @@ class Doc { } } -// ../../../node_modules/zod/v4/core/versions.js +// node_modules/zod/v4/core/versions.js var version = { major: 4, minor: 3, patch: 6 }; -// ../../../node_modules/zod/v4/core/schemas.js +// node_modules/zod/v4/core/schemas.js var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => { var _a; inst ?? (inst = {}); @@ -9289,7 +9289,7 @@ function handleRefineResult(result, payload, input, inst) { payload.issues.push(issue(_iss)); } } -// ../../../node_modules/zod/v4/locales/en.js +// node_modules/zod/v4/locales/en.js var error = () => { const Sizable = { string: { unit: "characters", verb: "to have" }, @@ -9395,7 +9395,7 @@ function en_default() { localeError: error() }; } -// ../../../node_modules/zod/v4/core/registries.js +// node_modules/zod/v4/core/registries.js var _a; var $output = Symbol("ZodOutput"); var $input = Symbol("ZodInput"); @@ -9445,7 +9445,7 @@ function registry() { } (_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry()); var globalRegistry = globalThis.__zod_globalRegistry; -// ../../../node_modules/zod/v4/core/api.js +// node_modules/zod/v4/core/api.js function _string(Class2, params) { return new Class2({ type: "string", @@ -9911,7 +9911,7 @@ function _check(fn, params) { ch._zod.check = fn; return ch; } -// ../../../node_modules/zod/v4/core/to-json-schema.js +// node_modules/zod/v4/core/to-json-schema.js function initializeContext(params) { let target = params?.target ?? "draft-2020-12"; if (target === "draft-4") @@ -10256,7 +10256,7 @@ var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) = extractDefs(ctx, schema); return finalize(ctx, schema); }; -// ../../../node_modules/zod/v4/core/json-schema-processors.js +// node_modules/zod/v4/core/json-schema-processors.js var formatMap = { guid: "uuid", url: "uri", @@ -10591,7 +10591,7 @@ var optionalProcessor = (schema, ctx, _json, params) => { const seen = ctx.seen.get(schema); seen.ref = def.innerType; }; -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js function isZ4Schema(s) { const schema = s; return !!schema._zod; @@ -10653,7 +10653,7 @@ function getLiteralValue(schema) { return directValue; return; } -// ../../../node_modules/zod/v4/classic/iso.js +// node_modules/zod/v4/classic/iso.js var exports_iso = {}; __export(exports_iso, { time: () => time2, @@ -10694,7 +10694,7 @@ function duration2(params) { return _isoDuration(ZodISODuration, params); } -// ../../../node_modules/zod/v4/classic/errors.js +// node_modules/zod/v4/classic/errors.js var initializer2 = (inst, issues) => { $ZodError.init(inst, issues); inst.name = "ZodError"; @@ -10729,7 +10729,7 @@ var ZodRealError = $constructor("ZodError", initializer2, { Parent: Error }); -// ../../../node_modules/zod/v4/classic/parse.js +// node_modules/zod/v4/classic/parse.js var parse3 = /* @__PURE__ */ _parse(ZodRealError); var parseAsync2 = /* @__PURE__ */ _parseAsync(ZodRealError); var safeParse3 = /* @__PURE__ */ _safeParse(ZodRealError); @@ -10743,7 +10743,7 @@ var safeDecode2 = /* @__PURE__ */ _safeDecode(ZodRealError); var safeEncodeAsync2 = /* @__PURE__ */ _safeEncodeAsync(ZodRealError); var safeDecodeAsync2 = /* @__PURE__ */ _safeDecodeAsync(ZodRealError); -// ../../../node_modules/zod/v4/classic/schemas.js +// node_modules/zod/v4/classic/schemas.js var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => { $ZodType.init(inst, def); Object.assign(inst["~standard"], { @@ -11382,10 +11382,10 @@ function superRefine(fn) { function preprocess(fn, schema) { return pipe(transform(fn), schema); } -// ../../../node_modules/zod/v4/classic/external.js +// node_modules/zod/v4/classic/external.js config(en_default()); -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/types.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/types.js var LATEST_PROTOCOL_VERSION = "2025-11-25"; var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"]; var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task"; @@ -12217,16 +12217,16 @@ class UrlElicitationRequiredError extends McpError { } } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js function isTerminal(status) { return status === "completed" || status === "failed" || status === "cancelled"; } -// ../../../node_modules/zod-to-json-schema/dist/esm/Options.js +// node_modules/zod-to-json-schema/dist/esm/Options.js var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use"); -// ../../../node_modules/zod-to-json-schema/dist/esm/parsers/string.js +// node_modules/zod-to-json-schema/dist/esm/parsers/string.js var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789"); -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js function getMethodLiteral(schema) { const shape = getObjectShape(schema); const methodSchema = shape?.method; @@ -12247,7 +12247,7 @@ function parseWithCompat(schema, data) { return result.data; } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js var DEFAULT_REQUEST_TIMEOUT_MSEC = 60000; class Protocol { @@ -13082,7 +13082,7 @@ function mergeCapabilities(base, additional) { return result; } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js var import_ajv = __toESM(require_ajv(), 1); var import_ajv_formats = __toESM(require_dist(), 1); function createDefaultAjvInstance() { @@ -13122,7 +13122,7 @@ class AjvJsonSchemaValidator { } } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js class ExperimentalServerTasks { constructor(_server) { this._server = _server; @@ -13200,7 +13200,7 @@ class ExperimentalServerTasks { } } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js function assertToolsCallTaskCapability(requests, method, entityName) { if (!requests) { throw new Error(`${entityName} does not support task creation (required for ${method})`); @@ -13235,7 +13235,7 @@ function assertClientRequestTaskCapability(requests, method, entityName) { } } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js class Server extends Protocol { constructor(_serverInfo, options) { super(options); @@ -13568,10 +13568,10 @@ class Server extends Protocol { } } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js import process3 from "process"; -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js class ReadBuffer { append(chunk) { this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; @@ -13601,7 +13601,7 @@ function serializeMessage(message) { `; } -// ../../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js +// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js class StdioServerTransport { constructor(_stdin = process3.stdin, _stdout = process3.stdout) { this._stdin = _stdin; @@ -14707,10 +14707,10 @@ function defineNumber(value, fallback) { } var BUILD_INFO = Object.freeze({ version: defineString("0.1.24", "0.0.0-source"), - commit: defineString("78a46a7", "source"), + commit: defineString("d486692", "source"), bundle: defineBundle("plugin"), contractVersion: defineNumber(1, CONTRACT_VERSION), - codeHash: defineString("0634265c8c47", "source") + codeHash: defineString("436cb8e8ab42", "source") }); function sameRuntimeContract(a, b) { if (!a || !b) diff --git a/plugins/agentbridge/server/daemon.js b/plugins/agentbridge/server/daemon.js index acc16fd..74a8288 100755 --- a/plugins/agentbridge/server/daemon.js +++ b/plugins/agentbridge/server/daemon.js @@ -30,10 +30,10 @@ function defineNumber(value, fallback) { } var BUILD_INFO = Object.freeze({ version: defineString("0.1.24", "0.0.0-source"), - commit: defineString("78a46a7", "source"), + commit: defineString("d486692", "source"), bundle: defineBundle("plugin"), contractVersion: defineNumber(1, CONTRACT_VERSION), - codeHash: defineString("0634265c8c47", "source") + codeHash: defineString("436cb8e8ab42", "source") }); function daemonStatusBuildInfo() { return { ...BUILD_INFO }; @@ -7023,7 +7023,6 @@ class BrokerClient { // src/room-service.ts import { realpathSync as realpathSync2 } from "fs"; - class RoomService { store; constructor(store) { diff --git a/src/broker-web.ts b/src/broker-web.ts new file mode 100644 index 0000000..338db77 --- /dev/null +++ b/src/broker-web.ts @@ -0,0 +1,259 @@ +/** + * `abg broker start` local admin dashboard. + * + * A LOOPBACK-ONLY (127.0.0.1) Bun.serve on its OWN port (default 4701), separate + * from the WS broker port. It lets the person running the broker SEE rooms / + * members / whiteboards and CREATE a room from a browser. + * + * Security: it is deliberately NOT exposed on the broker's 0.0.0.0 / Tailscale + * interface — that would be an UNAUTHENTICATED admin console reachable by anyone + * who can reach the broker (it would re-open the very access hole §11.2 closed). + * For remote viewing, SSH-forward this port or put it behind `tailscale serve` + * (which adds tailnet identity). A Host-header allowlist additionally defeats + * DNS-rebinding (a browser visiting evil.com→127.0.0.1 sends a non-loopback Host). + * Room creation reuses the same closed-by-default membership rules as the CLI. + */ + +import type { Store, WhiteboardRecord } from "./backbone/store"; +import { RoomService, slugify } from "./room-service"; + +export const DEFAULT_DASHBOARD_PORT = 4701; + +// The dashboard is loopback-only by design (unauthenticated admin console); any other +// bind host is refused at startBrokerWeb and forced back to 127.0.0.1. +const LOOPBACK_BIND = new Set(["127.0.0.1", "::1", "localhost"]); + +export interface BrokerWebOptions { + store: Store; + /** Bind host — loopback only. Do NOT change to a public interface without auth. */ + host?: string; + /** Bind port (default 4701; 0 = random, for tests). */ + port?: number; + /** Logged-in identity that owns rooms created from the UI; null = not logged in (create disabled). */ + createdBy: string | null; + log?: (m: string) => void; +} + +export interface BrokerWebHandle { + host: string; + port: number; + url: string; + stop(): void; +} + +interface RoomState { + roomId: string; + name: string; + createdBy: string; + members: string[]; + whiteboard: WhiteboardRecord | null; +} + +function errMsg(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +function json(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { "content-type": "application/json; charset=utf-8" }, + }); +} + +/** + * Only accept requests whose Host header is loopback:port. Bun already binds to + * 127.0.0.1, but this blocks DNS-rebinding (a malicious page resolving its own + * hostname to 127.0.0.1 would carry a non-loopback Host). Pure (takes the header + * string) so it is unit-testable without a live socket. + */ +export function hostAllowed(hostHeader: string | null, port: number): boolean { + if (!hostHeader) return false; + return ( + hostHeader === `127.0.0.1:${port}` || + hostHeader === `localhost:${port}` || + hostHeader === `[::1]:${port}` + ); +} + +async function collectState(store: Store): Promise<{ rooms: RoomState[]; generatedAt: number }> { + const svc = new RoomService(store); + const rooms = await svc.listRooms(); + const out: RoomState[] = []; + for (const r of rooms) { + out.push({ + roomId: r.roomId, + name: r.name, + createdBy: r.createdBy, + members: await store.getMembers(r.roomId), + whiteboard: await store.getWhiteboard(r.roomId), + }); + } + return { rooms: out, generatedAt: Date.now() }; +} + +/** + * Strip control + format chars (ANSI/newline/CR/zero-width) so an attacker-supplied room NAME can't + * inject sequences into the broker operator's terminal log (CWE-117). The web face is loopback-only, + * but the docs invite SSH / `tailscale serve` forwarding, so a forwarded POST body is untrusted. + * roomId is already slug-safe (slugify strips these); createdBy is the operator's own authenticated id. + */ +function safeForLog(s: string): string { + return s.replace(/[\p{Cc}\p{Cf}]/gu, ""); +} + +/** Create a room owned by the logged-in identity, mirroring the CLI's closed-by-default rules (§11.2). */ +async function createRoom(store: Store, createdBy: string | null, body: unknown, log: (m: string) => void): Promise { + if (!createdBy) return json({ error: "未登录:请在 broker 机上先运行 abg auth login" }, 401); + const name = typeof (body as { name?: unknown })?.name === "string" ? (body as { name: string }).name.trim() : ""; + if (!name) return json({ error: "缺少房间名称" }, 400); + let roomId: string; + try { + roomId = slugify(name); + } catch (e) { + return json({ error: errMsg(e) }, 400); + } + const svc = new RoomService(store); + const existed = (await svc.getRoom(roomId)) !== null; + if (!existed) { + await svc.createRoom(roomId, name, createdBy); + await svc.join(roomId, createdBy); // creator of a NEW room is its first member + log(`已创建房间 ${roomId}(${safeForLog(name)})by ${createdBy} — 加入:abg join ${roomId}`); + } else if (!(await svc.isMember(roomId, createdBy))) { + // Closed-by-default: creating an EXISTING room must not self-grant membership. + return json({ error: `房间 ${roomId} 已存在且你(${createdBy})不是成员;请让成员用 abg room add 加你` }, 409); + } + return json({ roomId, created: !existed }); +} + +/** Start the loopback admin dashboard. Returns a handle whose stop() shuts it down. */ +export function startBrokerWeb(opts: BrokerWebOptions): BrokerWebHandle { + const log = opts.log ?? (() => {}); + const requested = opts.host ?? "127.0.0.1"; + // The dashboard is an UNAUTHENTICATED admin console — it MUST bind loopback only. + // A non-loopback bind would expose every room/member/whiteboard + room creation to + // anyone who can reach it (HIGH). Enforce here regardless of the caller; fail SAFE + // to 127.0.0.1 (loudly) rather than trust the call site or a doc-comment. + const host = LOOPBACK_BIND.has(requested) ? requested : "127.0.0.1"; + if (host !== requested) + log(`忽略非 loopback 地址 ${requested}:管理面板无鉴权,强制绑 127.0.0.1(远程访问用 SSH 转发或 tailscale serve)`); + const server = Bun.serve({ + hostname: host, + port: opts.port ?? DEFAULT_DASHBOARD_PORT, + async fetch(req, srv) { + try { + if (!hostAllowed(req.headers.get("host"), srv.port ?? 0)) return new Response("forbidden host", { status: 403 }); + const url = new URL(req.url); + if (req.method === "GET" && url.pathname === "/") { + return new Response(DASHBOARD_HTML, { headers: { "content-type": "text/html; charset=utf-8" } }); + } + if (req.method === "GET" && url.pathname === "/api/state") { + return json(await collectState(opts.store)); + } + if (req.method === "POST" && url.pathname === "/api/rooms") { + let body: unknown = null; + try { + body = await req.json(); + } catch { + body = null; + } + return await createRoom(opts.store, opts.createdBy, body, log); + } + return new Response("not found", { status: 404 }); + } catch (e) { + log(`request failed: ${errMsg(e)}`); + return json({ error: "内部错误" }, 500); + } + }, + }); + // Server.port is number|undefined (undefined only for a unix socket, never used here). + const boundPort = server.port ?? opts.port ?? DEFAULT_DASHBOARD_PORT; + return { + host, + port: boundPort, + url: `http://${host}:${boundPort}`, + stop: () => void server.stop(true), // fire-and-forget: the dashboard does no critical DB writes + }; +} + +const DASHBOARD_HTML = ` + + + + +AgentBridge 房间面板 + + + +

🏠 AgentBridge 房间面板

+
+ + +
+
+
+ + +`; diff --git a/src/cli.ts b/src/cli.ts index c6fc7f8..6fa3e7e 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -189,6 +189,9 @@ Commands: room create | room list Create a collaboration room (id = slugified name) or list rooms join Join a room and auto-join this directory next time (§2.4) + broker start [--host ] [--port ] [--db ] [--web-port ] [--no-web] [--no-open] + Run the always-on control-plane broker (§11.1) + a loopback-only + admin dashboard (view rooms/members/whiteboards + create a room) logs [--codex] [-f] [-n N] Tail this pair's daemon log (or the codex wrapper log with --codex). -n N: last N lines (default 100). -f: follow/stream. diff --git a/src/cli/broker.ts b/src/cli/broker.ts index a9ac73e..632b61d 100644 --- a/src/cli/broker.ts +++ b/src/cli/broker.ts @@ -7,18 +7,13 @@ */ import { chmodSync, mkdirSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { dirname } from "node:path"; import { Broker, DEFAULT_BROKER_PORT } from "../broker"; import { SqliteStore } from "../backbone/store/sqlite-store"; import { StorePskIdentityProvider } from "../backbone/identity/store-psk-identity-provider"; -import { StateDirResolver } from "../state-dir"; - -function resolveDbPath(explicit?: string): string { - if (explicit) return explicit; - const env = process.env.AGENTBRIDGE_COLLAB_DB; - if (env && env.length > 0) return env; - return join(new StateDirResolver().dir, "collab.db"); -} +import { startBrokerWeb, DEFAULT_DASHBOARD_PORT } from "../broker-web"; +import { readAuthToken, resolveDbPath } from "../collab-store"; +import { detectTailscale, localIPv4s, buildConnectionCard } from "../net-detect"; const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]); @@ -51,10 +46,28 @@ export function resolveBindHost(raw: string): { host: string; warning: string | }; } +/** Best-effort: open `url` in the default browser. Never throws (a headless host just won't open). */ +function openBrowser(url: string, log: (m: string) => void): void { + const cmd = + process.platform === "darwin" + ? ["open", url] + : process.platform === "win32" + ? ["cmd", "/c", "start", "", url] + : ["xdg-open", url]; + try { + Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore", stdin: "ignore" }); + } catch (e) { + log(`无法自动打开浏览器:${e instanceof Error ? e.message : String(e)}`); + } +} + export async function runBrokerStart(argv: string[]): Promise { let host = "127.0.0.1"; let port = DEFAULT_BROKER_PORT; let db: string | undefined; + let webPort = DEFAULT_DASHBOARD_PORT; + let web = true; + let autoOpen = true; for (let i = 0; i < argv.length; i++) { const a = argv[i]!; if (a === "--host") host = argv[++i] ?? host; @@ -63,6 +76,10 @@ export async function runBrokerStart(argv: string[]): Promise { else if (a.startsWith("--port=")) port = parseInt(a.slice("--port=".length), 10); else if (a === "--db") db = argv[++i]; else if (a.startsWith("--db=")) db = a.slice("--db=".length); + else if (a === "--web-port") webPort = parseInt(argv[++i] ?? "", 10); + else if (a.startsWith("--web-port=")) webPort = parseInt(a.slice("--web-port=".length), 10); + else if (a === "--no-web") web = false; + else if (a === "--no-open") autoOpen = false; } if (!Number.isInteger(port) || port < 0 || port > 65535) { @@ -70,6 +87,11 @@ export async function runBrokerStart(argv: string[]): Promise { process.exit(1); return; } + if (web && (!Number.isInteger(webPort) || webPort < 0 || webPort > 65535)) { + console.error(`无效的 --web-port:${webPort}`); + process.exit(1); + return; + } const bind = resolveBindHost(host); if (bind.warning) console.error(bind.warning); @@ -92,20 +114,62 @@ export async function runBrokerStart(argv: string[]): Promise { console.log(`协作数据库:${dbPath}`); console.log("用 abg auth login 签发的 token 连接;Ctrl-C 停止。"); - // Graceful shutdown (§8.2): on SIGTERM/SIGINT, stop the server then close the - // Store — db.close() checkpoints the WAL so pending_deliveries survive the - // restart and a reconnecting member can still drain them. `once` so a second - // signal during teardown doesn't re-enter; exit 0 even if close() rejects. + // Graceful shutdown (§8.2): register IMMEDIATELY after bind — BEFORE the slow + // connectivity probe / web startup below — so a SIGTERM/Ctrl-C arriving right after + // the "已启动" line is handled (exit 0) rather than left to the default signal (kill). + // The handler reads `webHandle` lazily; if a signal beats web startup it's a no-op. + // db.close() checkpoints the WAL so pending_deliveries survive the restart. `once` + // so a second signal during teardown doesn't re-enter; exit 0 even if close() rejects. + let webHandle: { url: string; stop(): void } | undefined; let stopping = false; const shutdown = (sig: string) => { if (stopping) return; stopping = true; console.error(`[broker] ${sig} 收到,正在优雅关闭…`); + webHandle?.stop(); broker.stop(); store.close().finally(() => process.exit(0)); }; process.once("SIGTERM", () => shutdown("SIGTERM")); process.once("SIGINT", () => shutdown("SIGINT")); + + // Connectivity card (§ cross-network onboarding): detect Tailscale/LAN reach and + // print the exact connect command to hand a collaborator. Detection only — never + // auto-install or auto-invite (user decision: detect + guide). + try { + const card = buildConnectionCard({ + bindHost: bind.host, + brokerPort: bound.port, + tailscale: await detectTailscale(), + lanIps: localIPv4s(), + }); + console.log(""); + console.log("📡 网络可达性 / 邀请协作者:"); + for (const line of card.lines) console.log(line); + } catch { + // detection is best-effort; never block broker startup + } + + // Local admin dashboard (§ web console): a LOOPBACK-ONLY Bun.serve on its own + // port to view rooms/members/whiteboards + create a room from a browser. Never + // bound to the broker's public interface (that would be an unauthenticated admin + // console). For remote viewing: SSH-forward this port or `tailscale serve` it. + if (web) { + // Fail-inert: the dashboard is a convenience. If its port is taken or it fails + // to bind, log and keep the broker running — never let it take down the broker. + try { + const token = readAuthToken(dbPath); + const createdBy = token ? await store.resolveToken(token) : null; + webHandle = startBrokerWeb({ store, port: webPort, createdBy, log: (m) => console.error(`[web] ${m}`) }); + console.log(`管理面板(仅本机):${webHandle.url}`); + if (!createdBy) console.log("(未登录:面板可查看,建房需先 abg auth login)"); + const isSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY); + if (autoOpen && !isSsh) openBrowser(webHandle.url, (m) => console.error(`[web] ${m}`)); + else if (isSsh) console.log("检测到 SSH 会话,未自动打开浏览器;远程访问请 SSH 端口转发或 tailscale serve 该端口。"); + } catch (e) { + console.error(`[web] 管理面板启动失败(不影响 broker):${e instanceof Error ? e.message : String(e)}`); + } + } // Bun.serve keeps the event loop alive — the process stays up until a signal. } @@ -117,7 +181,9 @@ export async function runBroker(args: string[]): Promise { break; default: console.error(`未知的 broker 子命令:${sub ?? "(空)"}`); - console.error("用法:abg broker start [--host ] [--port ] [--db ]"); + console.error( + "用法:abg broker start [--host ] [--port ] [--db ] [--web-port ] [--no-web] [--no-open]", + ); process.exit(1); } } diff --git a/src/cli/room.ts b/src/cli/room.ts index 36d5cba..3a48d90 100644 --- a/src/cli/room.ts +++ b/src/cli/room.ts @@ -11,7 +11,7 @@ import { chmodSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; -import { RoomService } from "../room-service"; +import { RoomService, slugify } from "../room-service"; import { SqliteStore } from "../backbone/store/sqlite-store"; import type { RoomRecord, Store } from "../backbone/store"; import { StateDirResolver } from "../state-dir"; @@ -43,26 +43,6 @@ export async function currentIdentityId(store: Store, dbPath: string): Promise { test("SIGTERM stops the server, closes the Store, and exits 0", async () => { dir = mkdtempSync(join(tmpdir(), "agentbridge-broker-sig-")); const dbPath = join(dir, "collab.db"); - const child = spawn(process.execPath, ["run", CLI_PATH, "broker", "start", "--port", "0", "--db", dbPath], { + // --no-web: this test exercises broker shutdown only; keep it hermetic (no + // dashboard port bind / browser spawn). The shutdown-handler ordering fix is + // verified independently — the handler is registered before the web/probe step. + const child = spawn(process.execPath, ["run", CLI_PATH, "broker", "start", "--port", "0", "--db", dbPath, "--no-web"], { env: { ...process.env, AGENTBRIDGE_COLLAB_DB: dbPath }, stdio: ["ignore", "pipe", "pipe"], }); diff --git a/src/integration-test/broker-web.test.ts b/src/integration-test/broker-web.test.ts new file mode 100644 index 0000000..0ac63f8 --- /dev/null +++ b/src/integration-test/broker-web.test.ts @@ -0,0 +1,166 @@ +import { describe, test, expect, afterEach } from "bun:test"; +import { startBrokerWeb, hostAllowed, type BrokerWebHandle } from "../broker-web"; +import { InMemoryStore } from "../backbone/store/memory-store"; + +/** Seed a store with one room (alice+bob members) and a whiteboard. */ +async function seed(): Promise { + const store = new InMemoryStore(); + await store.createRoom("checkout", "结账重构", "alice@x.com"); + await store.addMember("checkout", "alice@x.com"); + await store.addMember("checkout", "bob@x.com"); + await store.saveWhiteboard("checkout", { + roomId: "checkout", + contractsReady: [{ contract: "auth/v1" }], + inProgress: [], + blockers: [], + recentMilestones: [{ summary: "auth done" }], + updatedAt: 1, + }); + return store; +} + +describe("broker web dashboard (loopback admin console)", () => { + let handle: BrokerWebHandle | undefined; + afterEach(() => { + handle?.stop(); + handle = undefined; + }); + + test("hostAllowed: only loopback:port passes (DNS-rebinding guard)", () => { + expect(hostAllowed("127.0.0.1:4701", 4701)).toBe(true); + expect(hostAllowed("localhost:4701", 4701)).toBe(true); + expect(hostAllowed("[::1]:4701", 4701)).toBe(true); + expect(hostAllowed("evil.example.com", 4701)).toBe(false); + expect(hostAllowed("127.0.0.1:9999", 4701)).toBe(false); // wrong port + expect(hostAllowed(null, 4701)).toBe(false); + }); + + test("GET /api/state returns rooms with members + whiteboard", async () => { + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: "alice@x.com" }); + const res = await fetch(`${handle.url}/api/state`); + expect(res.status).toBe(200); + const body = (await res.json()) as { + rooms: Array<{ roomId: string; name: string; createdBy: string; members: string[]; whiteboard: { contractsReady: unknown[] } | null }>; + }; + expect(body.rooms).toHaveLength(1); + expect(body.rooms[0]).toMatchObject({ roomId: "checkout", name: "结账重构", createdBy: "alice@x.com" }); + expect(body.rooms[0]!.members.sort()).toEqual(["alice@x.com", "bob@x.com"]); + expect(body.rooms[0]!.whiteboard!.contractsReady).toHaveLength(1); + }); + + test("GET / serves the dashboard HTML", async () => { + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: "alice@x.com" }); + const res = await fetch(handle.url); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/html"); + expect(await res.text()).toContain("房间面板"); + }); + + test("POST /api/rooms creates a room owned by the logged-in identity", async () => { + const store = await seed(); + handle = startBrokerWeb({ store, port: 0, createdBy: "alice@x.com" }); + const res = await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "新需求" }), + }); + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ roomId: "新需求", created: true }); + expect(await store.getMembers("新需求")).toEqual(["alice@x.com"]); // creator auto-joined + }); + + test("POST /api/rooms logs the new room id + join command to the broker terminal", async () => { + const logs: string[] = []; + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: "alice@x.com", log: (m) => logs.push(m) }); + await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "新需求" }), + }); + expect(logs.some((l) => l.includes("已创建房间 新需求") && l.includes("abg join 新需求"))).toBe(true); + }); + + test("POST /api/rooms strips control chars from the name before logging (no terminal injection, CWE-117)", async () => { + const logs: string[] = []; + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: "alice@x.com", log: (m) => logs.push(m) }); + await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "danger\u001b[2J\r\nFAKE" }), // ANSI clear-screen + CRLF forged-line attempt + }); + const line = logs.find((l) => l.includes("已创建房间")); + expect(line).toBeDefined(); + expect(line).not.toContain("\u001b"); // ESC stripped → no ANSI escape reaches the operator's terminal + expect(line).not.toContain("\n"); // newline stripped → can't forge a separate log line + expect(line).not.toContain("\r"); + }); + + test("GET / dashboard render builds a per-room `abg join ` command from r.roomId (not a static string)", async () => { + // Room cards render client-side from /api/state (asserted above), so GET / is the render() SOURCE — + // an empty store is fine. Assert render() CONCATENATES the command from EACH room's id, so dropping + // the join div or hardcoding the command fails here (the old toContain("abg join ") passed vacuously + // off any occurrence, even an unrendered one). + handle = startBrokerWeb({ store: new InMemoryStore(), port: 0, createdBy: "alice@x.com" }); + const html = await (await fetch(handle.url)).text(); + expect(html).toContain("abg join '+esc(r.roomId)"); // per-room, id-escaped — discriminating + }); + + test("POST /api/rooms is rejected when not logged in (createdBy null)", async () => { + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: null }); + const res = await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "x" }), + }); + expect(res.status).toBe(401); + }); + + test("POST /api/rooms with a punctuation-only name → 400 (no valid slug)", async () => { + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: "alice@x.com" }); + const res = await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "!!!" }), + }); + expect(res.status).toBe(400); + }); + + test("POST /api/rooms on an EXISTING room by a NON-member → 409 (closed-by-default, no self-grant)", async () => { + const store = await seed(); // room "checkout" has alice + bob, NOT mallory + handle = startBrokerWeb({ store, port: 0, createdBy: "mallory@x.com" }); + const res = await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "checkout" }), // slugifies to the existing room id + }); + expect(res.status).toBe(409); + expect(await store.getMembers("checkout")).not.toContain("mallory@x.com"); // never self-granted + }); + + test("POST /api/rooms on an EXISTING room by a MEMBER → 200 created:false (idempotent, no duplicate join)", async () => { + const store = await seed(); + handle = startBrokerWeb({ store, port: 0, createdBy: "alice@x.com" }); + const res = await fetch(`${handle.url}/api/rooms`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "checkout" }), + }); + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ roomId: "checkout", created: false }); + expect((await store.getMembers("checkout")).sort()).toEqual(["alice@x.com", "bob@x.com"]); // membership unchanged + }); + + test("unknown path → 404", async () => { + handle = startBrokerWeb({ store: await seed(), port: 0, createdBy: "alice@x.com" }); + const res = await fetch(`${handle.url}/nope`); + expect(res.status).toBe(404); + }); + + test("a non-loopback bind host is refused — forced to 127.0.0.1 (no public admin console)", () => { + handle = startBrokerWeb({ store: new InMemoryStore(), port: 0, createdBy: null, host: "0.0.0.0" }); + // Verifies OUR enforcement logic (the host handed to Bun.serve), not Bun's socket + // layer: a non-loopback request is forced back to 127.0.0.1 before binding. + expect(handle.host).toBe("127.0.0.1"); // enforced loopback regardless of the caller + expect(handle.url.startsWith("http://127.0.0.1:")).toBe(true); + }); +}); diff --git a/src/net-detect.ts b/src/net-detect.ts new file mode 100644 index 0000000..8a8e586 --- /dev/null +++ b/src/net-detect.ts @@ -0,0 +1,242 @@ +/** + * net-detect.ts — 网络可达性检测 + 连接卡生成(纯只读,无副作用除 tailscale 子命令) + * 供 `abg broker start` 打印「邀请别人加入的连接命令」,以及 join 侧探测 broker 可达性。 + */ + +import { networkInterfaces } from "os"; + +// ── 类型 ──────────────────────────────────────────────────────────────────── + +export interface TailscaleStatus { + installed: boolean; + running: boolean; + ipv4: string | null; + backendState: string | null; +} + +export interface ConnectionCard { + primary: string | null; + lines: string[]; +} + +// ── 内部工具 ───────────────────────────────────────────────────────────────── + +/** + * True iff `host` 在 Tailscale CGNAT 段 100.64.0.0/10(second octet 64–127)。 + * ponytail: 与 cli/broker.ts 中同名函数复制而非 import,避免循环耦合。 + */ +function isTailscaleCgnat(host: string): boolean { + const m = /^100\.(\d{1,3})\./.exec(host); + if (!m) return false; + const octet = Number(m[1]); + return octet >= 64 && octet <= 127; +} + +/** 是否是「具体可路由」地址(非 loopback、非全绑、非空)。 */ +function isPrimaryCandidate(host: string): boolean { + if (!host) return false; + if (new Set(["127.0.0.1", "::1", "localhost", "0.0.0.0", "::"]).has(host)) return false; + if (/^127\./.test(host)) return false; + return true; +} + +// ── detectTailscale ────────────────────────────────────────────────────────── + +const NULL_TS: TailscaleStatus = { installed: false, running: false, ipv4: null, backendState: null }; + +/** + * Spawn `tailscale status --json`,解析输出。 + * 命令不存在 / 超时(2s) / 非法 JSON / 任何错 → 返回 NULL_TS。绝不抛。 + */ +export async function detectTailscale(): Promise { + try { + const proc = Bun.spawn(["tailscale", "status", "--json"], { + stdout: "pipe", + stderr: "ignore", + stdin: "ignore", + }); + + const textP = new Response(proc.stdout).text(); + // ponytail: race + kill 作为 2s 超时;kill 让 stdout 关闭,textP 随即 resolve。 + let timer: ReturnType | undefined; + const timeoutP = new Promise((_, reject) => { + timer = setTimeout(() => { + try { proc.kill(); } catch { /* ignore */ } + reject(new Error("timeout")); + }, 2000); + }); + + const text = await Promise.race([textP, timeoutP]); + if (timer) clearTimeout(timer); // textP won — don't leak the 2s timer + const parsed = JSON.parse(text) as unknown; + if (typeof parsed !== "object" || parsed === null) return NULL_TS; + + const p = parsed as Record; + const backendState = typeof p.BackendState === "string" ? p.BackendState : null; + const running = backendState === "Running"; + const selfIps: unknown = (p.Self as Record | undefined)?.TailscaleIPs; + const ips: string[] = Array.isArray(selfIps) ? (selfIps as unknown[]).filter((x): x is string => typeof x === "string") : []; + const ipv4 = ips.find(isTailscaleCgnat) ?? null; + + return { installed: true, running, ipv4, backendState }; + } catch { + return NULL_TS; + } +} + +// ── localIPv4s ─────────────────────────────────────────────────────────────── + +/** os.networkInterfaces() 中所有非 internal、family IPv4、非 127.x 地址(去重)。 */ +export function localIPv4s(): string[] { + const seen = new Set(); + for (const ifaces of Object.values(networkInterfaces())) { + for (const iface of ifaces ?? []) { + if (!iface.internal && iface.family === "IPv4" && !iface.address.startsWith("127.")) { + seen.add(iface.address); + } + } + } + return [...seen]; +} + +// ── buildConnectionCard ────────────────────────────────────────────────────── + +/** + * 生成 `abg broker start` 要打印的「连接卡」行。 + * + * 主地址优先级: + * tailscale.running&&ipv4 → 100.x(跨网就绪) + * > bindHost 若是具体可路由地址 + * > lanIps[0](仅局域网,带提示) + * > null(仅本机 loopback,带提示) + */ +export function buildConnectionCard(opts: { + bindHost: string; + brokerPort: number; + tailscale: TailscaleStatus; + lanIps: string[]; +}): ConnectionCard { + const { bindHost, brokerPort, tailscale, lanIps } = opts; + + // ── 选 primary ── + let primary: string | null = null; + if (tailscale.running && tailscale.ipv4) { + primary = tailscale.ipv4; + } else if (isPrimaryCandidate(bindHost)) { + primary = bindHost; + } else if (lanIps.length > 0) { + primary = lanIps[0]!; + } + + const lines: string[] = []; + + // ── 地址清单 ── + if (tailscale.running && tailscale.ipv4) { + lines.push(` Tailscale: ${tailscale.ipv4} ✅ 跨网就绪`); + } else if (tailscale.installed) { + lines.push(` Tailscale: 未运行(${tailscale.backendState ?? "未知状态"})`); + } else { + lines.push(" Tailscale: 未安装"); + } + if (lanIps.length > 0) { + lines.push(` 局域网: ${lanIps.join(", ")} (仅局域网可达)`); + } + lines.push(` 本机: ${bindHost || "127.0.0.1"} (仅本机)`); + + // ── 协作者命令块 ── + lines.push(""); + lines.push("── 把下面的命令发给协作者 ──────────────────────────"); + if (primary) { + lines.push(` export AGENTBRIDGE_BROKER_URL=ws://${primary}:${brokerPort}/ws`); + } else { + lines.push(` # ⚠️ broker 只本机可达,跨机请先绑定可路由地址(见下方指引)`); + lines.push(` export AGENTBRIDGE_BROKER_URL=ws://127.0.0.1:${brokerPort}/ws`); + } + lines.push(" abg auth login --token <带外分发的 PSK>"); + lines.push(" abg join "); + + // ── Tailscale 未运行指引 ── + if (!tailscale.running) { + lines.push(""); + lines.push("── 对方没装 Tailscale?────────────────────────────"); + lines.push(" 1. 安装(对方自行确认并执行):"); + lines.push(" curl -fsSL https://tailscale.com/install.sh | sh"); + lines.push(" 2. 加入你的 tailnet(二选一):"); + lines.push(" • 管理员在 https://login.tailscale.com 发邀请链接"); + lines.push(" • tailscale up --authkey=<在管理面板生成的 pre-auth key>"); + } + + // ── 仅 loopback 额外提示 ── + if (!primary) { + lines.push(""); + lines.push(" ⚠️ broker 只本机可达,跨机需 --host 绑 Tailscale 100.x 或局域网 IP"); + } + + return { primary, lines }; +} + +// ── probeBroker ────────────────────────────────────────────────────────────── + +/** + * join 侧探测:尝试 WS 连 url,成功返回 ok:true。 + * 失败按现象给中文 hint。绝不抛。 + */ +export async function probeBroker( + url: string, + timeoutMs = 5000, +): Promise<{ ok: boolean; hint: string | null }> { + const hostMatch = /^wss?:\/\/([^/:]+)/.exec(url); + const host = hostMatch?.[1] ?? ""; + + // 提前查本机 Tailscale 状态,供 hint 分支判断 + const ts = await detectTailscale(); + + return new Promise((resolve) => { + let done = false; + const finish = (ok: boolean, hint: string | null): void => { + if (done) return; + done = true; + resolve({ ok, hint }); + }; + + let ws: WebSocket; + try { + ws = new WebSocket(url); + } catch { + finish(false, "连接地址格式错误或无法解析"); + return; + } + + const buildFailHint = (): string => { + if (isTailscaleCgnat(host) && !ts.running) { + return "你没连 tailnet,先执行 tailscale up 加入网络"; + } + return "连接被拒或超时——broker 可能没起,或地址/端口(默认 4700)不对,或对方未放行防火墙"; + }; + + const timer = setTimeout(() => { + try { ws.close(); } catch { /* ignore */ } + finish(false, buildFailHint()); + }, timeoutMs); + + ws.onopen = () => { + clearTimeout(timer); + finish(true, null); // set done BEFORE ws.close() so the resulting onclose is a no-op + ws.close(); + }; + + ws.onerror = () => { + clearTimeout(timer); + try { ws.close(); } catch { /* already closing */ } + finish(false, buildFailHint()); + }; + + // Closed before open (e.g. broker sends a WS Close on auth failure) → report + // unreachable now instead of waiting out the full timeout. The `done` guard makes + // this a no-op when onopen/onerror already settled (onopen's ws.close() fires it). + ws.onclose = () => { + clearTimeout(timer); + finish(false, buildFailHint()); + }; + }); +} diff --git a/src/room-service.ts b/src/room-service.ts index 6771841..538c003 100644 --- a/src/room-service.ts +++ b/src/room-service.ts @@ -1,6 +1,25 @@ import { realpathSync } from "node:fs"; import type { Store, RoomRecord } from "./backbone/store"; +/** + * Turn a human room name into a room id: lowercase, whitespace→`-`, keep unicode + * letters/numbers (Chinese-first, so "结账" is valid) + dash, drop everything else, + * collapse runs of `-`, trim leading/trailing `-`. Throws when nothing usable + * remains (e.g. a name of only punctuation). The room id is an internal topic / + * Store key (not a URL), so CJK is fine. Lives here (domain layer) so both the CLI + * and the broker web dashboard derive room ids identically. + */ +export function slugify(name: string): string { + const slug = name + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\p{L}\p{N}-]/gu, "") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); + if (slug === "") throw new Error(`无法从「${name}」生成有效的房间 ID(需含字母或数字)`); + return slug; +} + export interface AutoJoinResult { roomId: string; /** true if this call newly joined the agent; false if it was already a member. */ diff --git a/src/unit-test/net-detect.test.ts b/src/unit-test/net-detect.test.ts new file mode 100644 index 0000000..d8961a1 --- /dev/null +++ b/src/unit-test/net-detect.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, test } from "bun:test"; +import { + buildConnectionCard, + detectTailscale, + localIPv4s, + probeBroker, + type ConnectionCard, + type TailscaleStatus, +} from "../net-detect"; + +// ── localIPv4s ──────────────────────────────────────────────────────────────── + +describe("localIPv4s", () => { + test("返回数组,且不含 127.x loopback", () => { + const ips = localIPv4s(); + expect(Array.isArray(ips)).toBe(true); + for (const ip of ips) { + expect(ip).toBeString(); + expect(ip.startsWith("127.")).toBe(false); + } + }); +}); + +// ── buildConnectionCard — 4 分支 ────────────────────────────────────────────── + +describe("buildConnectionCard", () => { + const PORT = 4700; + + // 分支 1:tailscale running + ipv4 → primary = Tailscale 100.x + test("分支1 tailscale running+ipv4 → primary = 100.x", () => { + const ts: TailscaleStatus = { + installed: true, + running: true, + ipv4: "100.90.1.42", + backendState: "Running", + }; + const card: ConnectionCard = buildConnectionCard({ + bindHost: "127.0.0.1", + brokerPort: PORT, + tailscale: ts, + lanIps: ["192.168.1.5"], + }); + + expect(card.primary).toBe("100.90.1.42"); + // 命令块含 Tailscale IP + expect(card.lines.some((l) => l.includes("ws://100.90.1.42:4700/ws"))).toBe(true); + // 协作者命令块 + expect(card.lines.some((l) => l.includes("abg auth login --token"))).toBe(true); + expect(card.lines.some((l) => l.includes("abg join "))).toBe(true); + // tailscale running → 不显示「没装 Tailscale」指引 + expect(card.lines.some((l) => l.includes("没装 Tailscale"))).toBe(false); + }); + + // 分支 2:未装 Tailscale + 有局域网 IP → primary = lanIps[0] + test("分支2 未装 tailscale + 有 LAN IP → primary = lanIps[0]", () => { + const ts: TailscaleStatus = { + installed: false, + running: false, + ipv4: null, + backendState: null, + }; + const card: ConnectionCard = buildConnectionCard({ + bindHost: "127.0.0.1", + brokerPort: PORT, + tailscale: ts, + lanIps: ["192.168.0.100"], + }); + + expect(card.primary).toBe("192.168.0.100"); + expect(card.lines.some((l) => l.includes("ws://192.168.0.100:4700/ws"))).toBe(true); + expect(card.lines.some((l) => l.includes("abg auth login --token"))).toBe(true); + // tailscale 未运行 → 含「没装 Tailscale」指引 + expect(card.lines.some((l) => l.includes("没装 Tailscale"))).toBe(true); + expect(card.lines.some((l) => l.includes("tailscale.com/install.sh"))).toBe(true); + }); + + // 分支 3:未装 Tailscale + 仅 loopback → primary = null + test("分支3 未装 tailscale + 仅 loopback → primary = null,含仅本机可达提示", () => { + const ts: TailscaleStatus = { + installed: false, + running: false, + ipv4: null, + backendState: null, + }; + const card: ConnectionCard = buildConnectionCard({ + bindHost: "127.0.0.1", + brokerPort: PORT, + tailscale: ts, + lanIps: [], + }); + + expect(card.primary).toBeNull(); + // 含仅本机可达警告 + expect(card.lines.some((l) => l.includes("只本机可达"))).toBe(true); + // 仍然包含协作者命令块(fallback 地址) + expect(card.lines.some((l) => l.includes("abg auth login --token"))).toBe(true); + expect(card.lines.some((l) => l.includes("abg join "))).toBe(true); + // tailscale 未运行 → 含「没装 Tailscale」指引 + expect(card.lines.some((l) => l.includes("没装 Tailscale"))).toBe(true); + }); + + // 分支 4:bindHost 已是具体 100.x(tailscale 未运行)→ primary = bindHost + test("分支4 bindHost 是具体 100.x → primary = bindHost", () => { + const ts: TailscaleStatus = { + installed: false, + running: false, + ipv4: null, + backendState: null, + }; + const card: ConnectionCard = buildConnectionCard({ + bindHost: "100.90.0.5", + brokerPort: PORT, + tailscale: ts, + lanIps: [], + }); + + expect(card.primary).toBe("100.90.0.5"); + expect(card.lines.some((l) => l.includes("ws://100.90.0.5:4700/ws"))).toBe(true); + expect(card.lines.some((l) => l.includes("abg auth login --token"))).toBe(true); + }); +}); + +// ── detectTailscale ─────────────────────────────────────────────────────────── + +describe("detectTailscale", () => { + test("不抛,且返回结构完整", async () => { + const result = await detectTailscale(); + expect(result).toBeDefined(); + expect(typeof result.installed).toBe("boolean"); + expect(typeof result.running).toBe("boolean"); + // ipv4 / backendState 允许 null 或 string + expect(result.ipv4 === null || typeof result.ipv4 === "string").toBe(true); + expect(result.backendState === null || typeof result.backendState === "string").toBe(true); + }); +}); + +// ── probeBroker ─────────────────────────────────────────────────────────────── + +describe("probeBroker", () => { + test("连不上的地址 → ok:false,hint 非空", async () => { + // port 1 on loopback 必然被拒 + const result = await probeBroker("ws://127.0.0.1:1/ws", 4000); + expect(result.ok).toBe(false); + expect(typeof result.hint).toBe("string"); + expect((result.hint ?? "").length).toBeGreaterThan(0); + }, 8000 /* 给够 WS 连接 + detectTailscale 超时 */); +});