diff --git a/changelog.md b/changelog.md index 3aa00ba..55fde43 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-18 - 6.11.0 - feat(remoteingress) +add ability to generate remote ingress connection tokens and UI copy action; add hubDomain config option; update remoteingress dependency to ^3.1.1 + +- Add server typed handler 'getRemoteIngressConnectionToken' to generate an encoded connection token containing hubHost, hubPort, edgeId and secret. +- Add request interface IReq_GetRemoteIngressConnectionToken for typed requests. +- Add fetchConnectionToken helper in web appstate and a 'Copy Token' action in ops-view-remoteingress to copy tokens to the clipboard with toast feedback. +- Add hubDomain option to remoteIngressConfig in dcrouter options so an external hostname can be embedded in connection tokens. +- Bump dependency @serve.zone/remoteingress from ^3.0.4 to ^3.1.1 in package.json. + ## 2026-02-17 - 6.10.0 - feat(ops-view-certificates) Make Export and Delete actions available inline (inRow) as well as in the context menu; bump @design.estate/dees-catalog to ^3.43.0 diff --git a/package.json b/package.json index 54f9a26..cf29f90 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@push.rocks/smartstate": "^2.0.30", "@push.rocks/smartunique": "^3.0.9", "@serve.zone/interfaces": "^5.3.0", - "@serve.zone/remoteingress": "^3.0.4", + "@serve.zone/remoteingress": "^3.1.1", "@tsclass/tsclass": "^9.3.0", "lru-cache": "^11.2.6", "uuid": "^13.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0359fed..d5748ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: ^5.3.0 version: 5.3.0 '@serve.zone/remoteingress': - specifier: ^3.0.4 - version: 3.0.4 + specifier: ^3.1.1 + version: 3.1.1 '@tsclass/tsclass': specifier: ^9.3.0 version: 9.3.0 @@ -1340,8 +1340,8 @@ packages: '@serve.zone/interfaces@5.3.0': resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} - '@serve.zone/remoteingress@3.0.4': - resolution: {integrity: sha512-ZD66Y8fvW7SjealziOlhaC7+Y/3gxQkZlj/X8rxgVHmGhlc/YQtn6H6LNVazbM88BXK5ns004Qo6ongAB6Ho0Q==} + '@serve.zone/remoteingress@3.1.1': + resolution: {integrity: sha512-tTN3hkLmfL8KeIEu7a685xNlESEZ548aFzKn+44yeUwABuQV5w3P39Pk2U3KAGB0K2prnpuzBGuoMaEIicE9Ew==} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -6830,7 +6830,7 @@ snapshots: '@push.rocks/smartlog-interfaces': 3.0.2 '@tsclass/tsclass': 9.3.0 - '@serve.zone/remoteingress@3.0.4': + '@serve.zone/remoteingress@3.1.1': dependencies: '@push.rocks/qenv': 6.1.3 '@push.rocks/smartrust': 1.2.1 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index a45752d..7e2b39e 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '6.10.0', + version: '6.11.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 8799cf9..246f779 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -166,6 +166,8 @@ export interface IDcRouterOptions { enabled?: boolean; /** Port for tunnel connections from edge nodes (default: 8443) */ tunnelPort?: number; + /** External hostname of this hub, embedded in connection tokens */ + hubDomain?: string; /** TLS configuration for the tunnel server */ tls?: { certPath?: string; diff --git a/ts/opsserver/handlers/remoteingress.handler.ts b/ts/opsserver/handlers/remoteingress.handler.ts index 4b15443..f2d4a26 100644 --- a/ts/opsserver/handlers/remoteingress.handler.ts +++ b/ts/opsserver/handlers/remoteingress.handler.ts @@ -177,5 +177,46 @@ export class RemoteIngressHandler { }, ), ); + + // Get a connection token for an edge + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getRemoteIngressConnectionToken', + async (dataArg, toolsArg) => { + const manager = this.opsServerRef.dcRouterRef.remoteIngressManager; + if (!manager) { + return { success: false, message: 'RemoteIngress not configured' }; + } + + const edge = manager.getEdge(dataArg.edgeId); + if (!edge) { + return { success: false, message: 'Edge not found' }; + } + if (!edge.enabled) { + return { success: false, message: 'Edge is disabled' }; + } + + const hubHost = dataArg.hubHost + || this.opsServerRef.dcRouterRef.options.remoteIngressConfig?.hubDomain; + if (!hubHost) { + return { + success: false, + message: 'No hub hostname configured. Set hubDomain in remoteIngressConfig or provide hubHost.', + }; + } + + const hubPort = this.opsServerRef.dcRouterRef.options.remoteIngressConfig?.tunnelPort ?? 8443; + + const token = plugins.remoteingress.encodeConnectionToken({ + hubHost, + hubPort, + edgeId: edge.id, + secret: edge.secret, + }); + + return { success: true, token }; + }, + ), + ); } } diff --git a/ts_interfaces/requests/remoteingress.ts b/ts_interfaces/requests/remoteingress.ts index c4d33c6..a8b5269 100644 --- a/ts_interfaces/requests/remoteingress.ts +++ b/ts_interfaces/requests/remoteingress.ts @@ -117,3 +117,24 @@ export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfa statuses: IRemoteIngressStatus[]; }; } + +/** + * Get a connection token for a remote ingress edge. + * The token is a single opaque base64url string that encodes hubHost, hubPort, edgeId, and secret. + */ +export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedrequestInterfaces.implementsTR< + plugins.typedrequestInterfaces.ITypedRequest, + IReq_GetRemoteIngressConnectionToken +> { + method: 'getRemoteIngressConnectionToken'; + request: { + identity?: authInterfaces.IIdentity; + edgeId: string; + hubHost?: string; + }; + response: { + success: boolean; + token?: string; + message?: string; + }; +} diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index a45752d..7e2b39e 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '6.10.0', + version: '6.11.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 7ff7662..115c25b 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -854,6 +854,18 @@ export async function fetchCertificateExport(domain: string) { }); } +// ============================================================================ +// Remote Ingress Standalone Functions +// ============================================================================ + +export async function fetchConnectionToken(edgeId: string) { + const context = getActionContext(); + const request = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_GetRemoteIngressConnectionToken + >('/typedrequest', 'getRemoteIngressConnectionToken'); + return request.fire({ identity: context.identity, edgeId }); +} + // ============================================================================ // Remote Ingress Actions // ============================================================================ diff --git a/ts_web/elements/ops-view-remoteingress.ts b/ts_web/elements/ops-view-remoteingress.ts index ddcee7f..ad3288e 100644 --- a/ts_web/elements/ops-view-remoteingress.ts +++ b/ts_web/elements/ops-view-remoteingress.ts @@ -346,6 +346,26 @@ export class OpsViewRemoteIngress extends DeesElement { ); }, }, + { + name: 'Copy Token', + iconName: 'lucide:clipboard-copy', + type: ['inRow', 'contextmenu'] as any, + actionFunc: async (actionData: any) => { + const edge = actionData.item as interfaces.data.IRemoteIngress; + const { DeesToast } = await import('@design.estate/dees-catalog'); + try { + const response = await appstate.fetchConnectionToken(edge.id); + if (response.success && response.token) { + await navigator.clipboard.writeText(response.token); + DeesToast.show({ message: `Connection token copied for ${edge.name}`, type: 'success', duration: 3000 }); + } else { + DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 }); + } + } catch (err) { + DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 }); + } + }, + }, { name: 'Delete', iconName: 'lucide:trash2',