@serve.zone/dcrouter 17.7.1 → 17.9.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/deno.json +1 -1
- package/dist_serve/bundle.js +712 -709
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -0
- package/dist_ts/classes.dcrouter.js +23 -1
- package/dist_ts/config/classes.db-seeder.js +9 -1
- package/dist_ts/config/classes.gateway-client-manager.js +3 -1
- package/dist_ts/config/helpers.http-redirects.js +28 -1
- package/dist_ts/config/helpers.special-forwards.js +12 -9
- package/dist_ts/db/documents/classes.cached.email.d.ts +7 -0
- package/dist_ts/db/documents/classes.cached.email.js +52 -2
- package/dist_ts/db/documents/classes.webpush-admission.doc.d.ts +19 -0
- package/dist_ts/db/documents/classes.webpush-admission.doc.js +124 -0
- package/dist_ts/db/documents/classes.webpush-binding.doc.d.ts +65 -0
- package/dist_ts/db/documents/classes.webpush-binding.doc.js +205 -0
- package/dist_ts/db/documents/classes.webpush-spool.doc.d.ts +44 -0
- package/dist_ts/db/documents/classes.webpush-spool.doc.js +277 -0
- package/dist_ts/db/documents/index.d.ts +3 -0
- package/dist_ts/db/documents/index.js +5 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +5 -0
- package/dist_ts/email/classes.accepted-email-spool.js +28 -3
- package/dist_ts/email/classes.email-route-builder.js +5 -1
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +5 -1
- package/dist_ts/email/classes.workapp-mail-manager.js +87 -12
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
- package/dist_ts/opsserver/classes.opsserver.js +5 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +33 -6
- package/dist_ts/opsserver/handlers/index.d.ts +2 -0
- package/dist_ts/opsserver/handlers/index.js +3 -1
- package/dist_ts/opsserver/handlers/webpush-app.handler.d.ts +9 -0
- package/dist_ts/opsserver/handlers/webpush-app.handler.js +66 -0
- package/dist_ts/opsserver/handlers/webpush-control.handler.d.ts +14 -0
- package/dist_ts/opsserver/handlers/webpush-control.handler.js +163 -0
- package/dist_ts/plugins.d.ts +5 -2
- package/dist_ts/plugins.js +6 -3
- package/dist_ts/remoteingress/edge-derivation-routes.js +5 -1
- package/dist_ts/security/classes.route-policy-augmenter.d.ts +10 -6
- package/dist_ts/security/classes.route-policy-augmenter.js +65 -51
- package/dist_ts/webpush/classes.webpush-crypto.d.ts +38 -0
- package/dist_ts/webpush/classes.webpush-crypto.js +231 -0
- package/dist_ts/webpush/classes.webpush-manager.d.ts +153 -0
- package/dist_ts/webpush/classes.webpush-manager.js +2374 -0
- package/dist_ts/webpush/classes.webpush-transport.d.ts +45 -0
- package/dist_ts/webpush/classes.webpush-transport.js +161 -0
- package/dist_ts/webpush/helpers.webpush-endpoint.d.ts +21 -0
- package/dist_ts/webpush/helpers.webpush-endpoint.js +111 -0
- package/dist_ts/webpush/index.d.ts +4 -0
- package/dist_ts/webpush/index.js +5 -0
- package/dist_ts/webpush/webpush-indexes.d.ts +11 -0
- package/dist_ts/webpush/webpush-indexes.js +123 -0
- package/dist_ts_interfaces/data/remoteingress.d.ts +14 -2
- package/dist_ts_interfaces/data/route-management.d.ts +2 -0
- package/dist_ts_interfaces/data/route-management.js +1 -1
- package/dist_ts_migrations/index.js +285 -11
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.js +41 -1
- package/dist_ts_web/elements/network/special-forward-dialog.js +2 -2
- package/package.json +14 -10
- package/readme.md +27 -0
- package/third-party-notices.md +37 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +29 -0
- package/ts/config/classes.db-seeder.ts +8 -0
- package/ts/config/classes.gateway-client-manager.ts +2 -0
- package/ts/config/helpers.http-redirects.ts +31 -1
- package/ts/config/helpers.special-forwards.ts +11 -8
- package/ts/db/documents/classes.cached.email.ts +47 -0
- package/ts/db/documents/classes.webpush-admission.doc.ts +48 -0
- package/ts/db/documents/classes.webpush-binding.doc.ts +147 -0
- package/ts/db/documents/classes.webpush-spool.doc.ts +136 -0
- package/ts/db/documents/index.ts +5 -0
- package/ts/email/classes.accepted-email-spool.ts +41 -2
- package/ts/email/classes.email-route-builder.ts +4 -0
- package/ts/email/classes.workapp-mail-manager.ts +97 -10
- package/ts/index.ts +3 -0
- package/ts/opsserver/classes.opsserver.ts +4 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +34 -4
- package/ts/opsserver/handlers/index.ts +2 -0
- package/ts/opsserver/handlers/webpush-app.handler.ts +105 -0
- package/ts/opsserver/handlers/webpush-control.handler.ts +257 -0
- package/ts/plugins.ts +6 -0
- package/ts/remoteingress/edge-derivation-routes.ts +4 -0
- package/ts/security/classes.route-policy-augmenter.ts +70 -57
- package/ts/webpush/classes.webpush-crypto.ts +306 -0
- package/ts/webpush/classes.webpush-manager.ts +3173 -0
- package/ts/webpush/classes.webpush-transport.ts +241 -0
- package/ts/webpush/helpers.webpush-endpoint.ts +150 -0
- package/ts/webpush/index.ts +4 -0
- package/ts/webpush/webpush-indexes.ts +141 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +38 -0
- package/ts_web/elements/network/special-forward-dialog.ts +1 -1
|
@@ -98,6 +98,10 @@ export class EmailRouteBuilder {
|
|
|
98
98
|
},
|
|
99
99
|
action: action
|
|
100
100
|
};
|
|
101
|
+
routeConfig.ingress = {
|
|
102
|
+
directHub: !this.dcRouterRef.isRemoteIngressHubEnabled(),
|
|
103
|
+
smartVpn: !this.dcRouterRef.isRemoteIngressHubEnabled(),
|
|
104
|
+
};
|
|
101
105
|
|
|
102
106
|
if (this.dcRouterRef.isRemoteIngressHubEnabled()) {
|
|
103
107
|
// Only mail-tagged edges bind SMTP ports — other edges may sit on hosts
|
|
@@ -383,6 +383,7 @@ export class WorkAppMailManager {
|
|
|
383
383
|
authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth,
|
|
384
384
|
messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload,
|
|
385
385
|
outboundIdentityIdArg?: string,
|
|
386
|
+
idempotencyKeyArg?: string,
|
|
386
387
|
): Promise<TMailEnqueueResponse> {
|
|
387
388
|
if (!this.dcRouterRef.emailServer) {
|
|
388
389
|
return { accepted: false, message: 'Email server is not running' };
|
|
@@ -394,19 +395,41 @@ export class WorkAppMailManager {
|
|
|
394
395
|
outboundIdentityIdArg,
|
|
395
396
|
);
|
|
396
397
|
await this.assertIdentityOutboundReady(identity);
|
|
398
|
+
const idempotencyKey = this.normalizeMailIdempotencyKey(idempotencyKeyArg);
|
|
399
|
+
const submissionDigest = idempotencyKey
|
|
400
|
+
? this.getMailSubmissionDigest(identity, messageArg)
|
|
401
|
+
: undefined;
|
|
402
|
+
const existingReplay = idempotencyKey
|
|
403
|
+
? await this.getMailSubmissionReplay(identity, idempotencyKey, submissionDigest!)
|
|
404
|
+
: undefined;
|
|
405
|
+
if (existingReplay) return existingReplay;
|
|
397
406
|
const rawMessage = this.buildOutboundRawMessage(identity, messageArg);
|
|
398
407
|
const recipients = this.getMessageRecipients(messageArg);
|
|
399
408
|
const session = this.buildAuthenticatedSession(identity, messageArg.from, recipients, rawMessage);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
409
|
+
let acceptance;
|
|
410
|
+
try {
|
|
411
|
+
acceptance = await this.dcRouterRef.acceptedEmailSpool.acceptRawMessage({
|
|
412
|
+
rawMessage,
|
|
413
|
+
envelope: {
|
|
414
|
+
mailFrom: identity.address,
|
|
415
|
+
rcptTo: recipients,
|
|
416
|
+
},
|
|
417
|
+
session,
|
|
418
|
+
messageId: this.extractHeader(rawMessage, 'message-id'),
|
|
419
|
+
subject: messageArg.subject,
|
|
420
|
+
...(idempotencyKey ? {
|
|
421
|
+
submissionCredentialId: identity.id,
|
|
422
|
+
submissionIdempotencyKey: idempotencyKey,
|
|
423
|
+
submissionDigest,
|
|
424
|
+
} : {}),
|
|
425
|
+
});
|
|
426
|
+
} catch (errorArg) {
|
|
427
|
+
const racedReplay = idempotencyKey
|
|
428
|
+
? await this.getMailSubmissionReplay(identity, idempotencyKey, submissionDigest!)
|
|
429
|
+
: undefined;
|
|
430
|
+
if (racedReplay) return racedReplay;
|
|
431
|
+
throw errorArg;
|
|
432
|
+
}
|
|
410
433
|
return {
|
|
411
434
|
accepted: acceptance.accepted,
|
|
412
435
|
spoolItemId: acceptance.spoolItemId,
|
|
@@ -421,6 +444,70 @@ export class WorkAppMailManager {
|
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
|
|
447
|
+
private normalizeMailIdempotencyKey(valueArg?: string): string | undefined {
|
|
448
|
+
if (valueArg === undefined) return undefined;
|
|
449
|
+
if (
|
|
450
|
+
typeof valueArg !== 'string'
|
|
451
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9:._/-]{0,199}$/.test(valueArg)
|
|
452
|
+
) throw new Error('Mail idempotencyKey is invalid');
|
|
453
|
+
return valueArg;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private canonicalMailSubmissionJson(valueArg: unknown): string {
|
|
457
|
+
if (valueArg === null || typeof valueArg === 'string' || typeof valueArg === 'boolean') {
|
|
458
|
+
return JSON.stringify(valueArg);
|
|
459
|
+
}
|
|
460
|
+
if (typeof valueArg === 'number') {
|
|
461
|
+
if (!Number.isFinite(valueArg)) throw new Error('Mail submission contains an invalid number');
|
|
462
|
+
return JSON.stringify(valueArg);
|
|
463
|
+
}
|
|
464
|
+
if (Array.isArray(valueArg)) {
|
|
465
|
+
return `[${valueArg.map((itemArg) => this.canonicalMailSubmissionJson(itemArg)).join(',')}]`;
|
|
466
|
+
}
|
|
467
|
+
if (valueArg && typeof valueArg === 'object') {
|
|
468
|
+
const entries = Object.entries(valueArg as Record<string, unknown>)
|
|
469
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
470
|
+
.sort(([leftKey], [rightKey]) => leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0);
|
|
471
|
+
return `{${entries.map(([keyArg, entryValue]) => (
|
|
472
|
+
`${JSON.stringify(keyArg)}:${this.canonicalMailSubmissionJson(entryValue)}`
|
|
473
|
+
)).join(',')}}`;
|
|
474
|
+
}
|
|
475
|
+
throw new Error('Mail submission contains an unsupported value');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private getMailSubmissionDigest(
|
|
479
|
+
identityArg: IStoredWorkAppMailIdentity,
|
|
480
|
+
messageArg: plugins.servezoneInterfaces.data.IMailOutboundMessagePayload,
|
|
481
|
+
): string {
|
|
482
|
+
const canonical = this.canonicalMailSubmissionJson({
|
|
483
|
+
identityId: identityArg.id,
|
|
484
|
+
message: messageArg,
|
|
485
|
+
version: 1,
|
|
486
|
+
});
|
|
487
|
+
return `sha256:${plugins.crypto.createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private async getMailSubmissionReplay(
|
|
491
|
+
identityArg: IStoredWorkAppMailIdentity,
|
|
492
|
+
idempotencyKeyArg: string,
|
|
493
|
+
submissionDigestArg: string,
|
|
494
|
+
): Promise<TMailEnqueueResponse | undefined> {
|
|
495
|
+
const existing = await CachedEmail.findBySubmissionIdempotencyKey(
|
|
496
|
+
identityArg.id,
|
|
497
|
+
idempotencyKeyArg,
|
|
498
|
+
);
|
|
499
|
+
if (!existing) return undefined;
|
|
500
|
+
if (existing.submissionDigest !== submissionDigestArg) {
|
|
501
|
+
throw new Error('Mail idempotencyKey was reused with different message semantics');
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
accepted: true,
|
|
505
|
+
spoolItemId: existing.id,
|
|
506
|
+
outboundIdentity: this.toMailOutboundIdentity(identityArg),
|
|
507
|
+
message: 'Idempotent service-mail replay',
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
424
511
|
public async getMailDeliveryStatus(
|
|
425
512
|
spoolItemIdArg: string,
|
|
426
513
|
): Promise<TMailDeliveryStatusResponse> {
|
package/ts/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ export * from './radius/index.js';
|
|
|
15
15
|
// Remote Ingress module
|
|
16
16
|
export * from './remoteingress/index.js';
|
|
17
17
|
|
|
18
|
+
// Web Push provider module
|
|
19
|
+
export * from './webpush/index.js';
|
|
20
|
+
|
|
18
21
|
// HTTP/3 module
|
|
19
22
|
export type { IHttp3Config } from './http3/index.js';
|
|
20
23
|
|
|
@@ -41,6 +41,8 @@ export class OpsServer {
|
|
|
41
41
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
42
42
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
43
43
|
private gatewayClientHandler!: handlers.GatewayClientHandler;
|
|
44
|
+
private webPushControlHandler!: handlers.WebPushControlHandler;
|
|
45
|
+
private webPushAppHandler!: handlers.WebPushAppHandler;
|
|
44
46
|
private configEventHandler!: handlers.ConfigEventHandler;
|
|
45
47
|
private smtpRealtimeUnsubscribe?: () => void;
|
|
46
48
|
private emailQueueRealtimeUnsubscribe?: () => void;
|
|
@@ -106,6 +108,8 @@ export class OpsServer {
|
|
|
106
108
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
107
109
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
108
110
|
this.gatewayClientHandler = new handlers.GatewayClientHandler(this);
|
|
111
|
+
this.webPushControlHandler = new handlers.WebPushControlHandler(this);
|
|
112
|
+
this.webPushAppHandler = new handlers.WebPushAppHandler(this);
|
|
109
113
|
this.configEventHandler = new handlers.ConfigEventHandler(this);
|
|
110
114
|
this.smtpRealtimeUnsubscribe =
|
|
111
115
|
this.dcRouterRef.acceptedEmailSpool?.onSmtpTransactionPersisted?.((eventArg) => {
|
|
@@ -524,6 +524,7 @@ export class GatewayClientHandler {
|
|
|
524
524
|
dataArg.auth || {},
|
|
525
525
|
dataArg.message,
|
|
526
526
|
dataArg.outboundIdentityId,
|
|
527
|
+
dataArg.idempotencyKey,
|
|
527
528
|
);
|
|
528
529
|
},
|
|
529
530
|
),
|
|
@@ -609,6 +610,12 @@ export class GatewayClientHandler {
|
|
|
609
610
|
http3: {
|
|
610
611
|
enabled: dcRouter.options.http3?.enabled !== false,
|
|
611
612
|
},
|
|
613
|
+
webPush: {
|
|
614
|
+
bindings: Boolean(dcRouter.webPushManager?.isReady),
|
|
615
|
+
delivery: Boolean(dcRouter.webPushManager?.isReady),
|
|
616
|
+
cancellation: Boolean(dcRouter.webPushManager?.isReady),
|
|
617
|
+
vapidRotation: Boolean(dcRouter.webPushManager?.isReady),
|
|
618
|
+
},
|
|
612
619
|
};
|
|
613
620
|
}
|
|
614
621
|
|
|
@@ -1346,9 +1353,9 @@ export class GatewayClientHandler {
|
|
|
1346
1353
|
if (!sourceBindings) {
|
|
1347
1354
|
return {
|
|
1348
1355
|
success: false,
|
|
1349
|
-
message: sourceProfileRef
|
|
1356
|
+
message: sourceProfileRef?.trim()
|
|
1350
1357
|
? `source profile '${sourceProfileRef}' not found`
|
|
1351
|
-
: '
|
|
1358
|
+
: 'STANDARD source profile not found',
|
|
1352
1359
|
};
|
|
1353
1360
|
}
|
|
1354
1361
|
|
|
@@ -1362,6 +1369,19 @@ export class GatewayClientHandler {
|
|
|
1362
1369
|
gatewayDnsMode: dnsMode,
|
|
1363
1370
|
};
|
|
1364
1371
|
const normalizedRoute = this.normalizeGatewayClientRoute(validatedRoute, resolvedOwnership, externalKey);
|
|
1372
|
+
if (!normalizedRoute.ingress) {
|
|
1373
|
+
const existingIngress = existingRoute?.route.ingress;
|
|
1374
|
+
normalizedRoute.ingress = existingIngress
|
|
1375
|
+
? structuredClone(existingIngress)
|
|
1376
|
+
: {
|
|
1377
|
+
directHub: true,
|
|
1378
|
+
smartVpn: true,
|
|
1379
|
+
};
|
|
1380
|
+
console.warn(
|
|
1381
|
+
`Gateway client route '${externalKey}' omitted route.ingress; `
|
|
1382
|
+
+ `${existingIngress ? 'preserved stored policy' : 'applied legacy directHub+smartVpn compatibility policy'}`,
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1365
1385
|
|
|
1366
1386
|
if (existingRoute) {
|
|
1367
1387
|
const snapshot = structuredClone(existingRoute);
|
|
@@ -1589,7 +1609,7 @@ export class GatewayClientHandler {
|
|
|
1589
1609
|
};
|
|
1590
1610
|
|
|
1591
1611
|
const route = requireObject(routeArg, 'route');
|
|
1592
|
-
requireOnlyKeys(route, ['name', 'match', 'action', 'priority', 'managedRouteKind', 'remoteIngress'], 'route');
|
|
1612
|
+
requireOnlyKeys(route, ['name', 'match', 'action', 'priority', 'managedRouteKind', 'ingress', 'remoteIngress'], 'route');
|
|
1593
1613
|
const name = requireString(route.name, 'route.name');
|
|
1594
1614
|
|
|
1595
1615
|
const match = requireObject(route.match, 'route.match');
|
|
@@ -1663,6 +1683,15 @@ export class GatewayClientHandler {
|
|
|
1663
1683
|
if (route.managedRouteKind !== 'letsencrypt-http01-forward') fail('route.managedRouteKind is unsupported');
|
|
1664
1684
|
managedRouteKind = route.managedRouteKind as 'letsencrypt-http01-forward';
|
|
1665
1685
|
}
|
|
1686
|
+
let ingress: { directHub: boolean; smartVpn: boolean } | undefined;
|
|
1687
|
+
if (route.ingress !== undefined) {
|
|
1688
|
+
const ingressValue = requireObject(route.ingress, 'route.ingress');
|
|
1689
|
+
requireOnlyKeys(ingressValue, ['directHub', 'smartVpn'], 'route.ingress');
|
|
1690
|
+
ingress = {
|
|
1691
|
+
directHub: requireBoolean(ingressValue.directHub, 'route.ingress.directHub'),
|
|
1692
|
+
smartVpn: requireBoolean(ingressValue.smartVpn, 'route.ingress.smartVpn'),
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1666
1695
|
let remoteIngress: { enabled: boolean; edgeFilter?: string[] } | undefined;
|
|
1667
1696
|
if (route.remoteIngress !== undefined) {
|
|
1668
1697
|
const remoteValue = requireObject(route.remoteIngress, 'route.remoteIngress');
|
|
@@ -1687,6 +1716,7 @@ export class GatewayClientHandler {
|
|
|
1687
1716
|
action: { type: 'forward', targets, ...(tls ? { tls } : {}), ...(websocket ? { websocket } : {}) },
|
|
1688
1717
|
...(priority !== undefined ? { priority } : {}),
|
|
1689
1718
|
...(managedRouteKind ? { managedRouteKind } : {}),
|
|
1719
|
+
...(ingress ? { ingress } : {}),
|
|
1690
1720
|
...(remoteIngress ? { remoteIngress } : {}),
|
|
1691
1721
|
};
|
|
1692
1722
|
}
|
|
@@ -1747,7 +1777,7 @@ export class GatewayClientHandler {
|
|
|
1747
1777
|
): interfaces.data.IRouteSourceBinding[] | undefined {
|
|
1748
1778
|
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
1749
1779
|
const profiles = resolver?.listProfiles() || [];
|
|
1750
|
-
const wantedRef = (requestedProfileRefArg || '
|
|
1780
|
+
const wantedRef = (requestedProfileRefArg?.trim() || 'standard').toLowerCase();
|
|
1751
1781
|
const profile = profiles.find((profileArg: interfaces.data.ISourceProfile) => {
|
|
1752
1782
|
return profileArg.id.trim().toLowerCase() === wantedRef;
|
|
1753
1783
|
}) || profiles.find((profileArg: interfaces.data.ISourceProfile) => {
|
|
@@ -23,3 +23,5 @@ export * from './email-domain.handler.js';
|
|
|
23
23
|
export * from './gatewayclient.handler.js';
|
|
24
24
|
export * from './realtime.handler.js';
|
|
25
25
|
export * from './config-event.handler.js';
|
|
26
|
+
export * from './webpush-control.handler.js';
|
|
27
|
+
export * from './webpush-app.handler.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
|
|
4
|
+
export class WebPushAppHandler {
|
|
5
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
6
|
+
|
|
7
|
+
public constructor(private readonly opsServerRef: OpsServer) {
|
|
8
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
9
|
+
this.registerHandlers();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private registerHandlers(): void {
|
|
13
|
+
this.typedrouter.addTypedHandler(
|
|
14
|
+
new plugins.typedrequest.TypedHandler<
|
|
15
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_GetWebPushServiceStatus
|
|
16
|
+
>(
|
|
17
|
+
'getWebPushServiceStatus',
|
|
18
|
+
async (dataArg) => {
|
|
19
|
+
const manager = this.opsServerRef.dcRouterRef.webPushManager;
|
|
20
|
+
if (!manager.isReady) return manager.getUnavailableServiceStatus();
|
|
21
|
+
return await manager.getServiceStatus(dataArg.auth);
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
this.typedrouter.addTypedHandler(
|
|
27
|
+
new plugins.typedrequest.TypedHandler<
|
|
28
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_EnqueueWebPush
|
|
29
|
+
>(
|
|
30
|
+
'enqueueWebPush',
|
|
31
|
+
async (dataArg) => {
|
|
32
|
+
try {
|
|
33
|
+
return await this.opsServerRef.dcRouterRef.webPushManager.enqueue(
|
|
34
|
+
dataArg.auth,
|
|
35
|
+
{
|
|
36
|
+
idempotencyKey: dataArg.idempotencyKey,
|
|
37
|
+
subscriptionId: dataArg.subscriptionId,
|
|
38
|
+
subscription: dataArg.subscription,
|
|
39
|
+
vapidKeyId: dataArg.vapidKeyId,
|
|
40
|
+
payload: dataArg.payload,
|
|
41
|
+
ttlSeconds: dataArg.ttlSeconds,
|
|
42
|
+
urgency: dataArg.urgency,
|
|
43
|
+
collapseKey: dataArg.collapseKey,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
} catch (error: unknown) {
|
|
47
|
+
return {
|
|
48
|
+
accepted: false,
|
|
49
|
+
message: this.safeMessage(error),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
this.typedrouter.addTypedHandler(
|
|
57
|
+
new plugins.typedrequest.TypedHandler<
|
|
58
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_CancelWebPush
|
|
59
|
+
>(
|
|
60
|
+
'cancelWebPush',
|
|
61
|
+
async (dataArg) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await this.opsServerRef.dcRouterRef.webPushManager.cancel(
|
|
64
|
+
dataArg.auth,
|
|
65
|
+
dataArg.target,
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
...result,
|
|
70
|
+
};
|
|
71
|
+
} catch (error: unknown) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
cancelledCount: 0,
|
|
75
|
+
alreadyTerminalCount: 0,
|
|
76
|
+
message: this.safeMessage(error),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
this.typedrouter.addTypedHandler(
|
|
84
|
+
new plugins.typedrequest.TypedHandler<
|
|
85
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_GetWebPushDeliveryStatus
|
|
86
|
+
>(
|
|
87
|
+
'getWebPushDeliveryStatus',
|
|
88
|
+
async (dataArg) => {
|
|
89
|
+
return {
|
|
90
|
+
delivery: await this.opsServerRef.dcRouterRef.webPushManager.getDeliveryStatus(
|
|
91
|
+
dataArg.auth,
|
|
92
|
+
dataArg.spoolItemId,
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private safeMessage(errorArg: unknown): string {
|
|
101
|
+
return errorArg instanceof plugins.typedrequest.TypedResponseError
|
|
102
|
+
? errorArg.message
|
|
103
|
+
: 'Web Push delivery operation failed';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import type { WebPushBindingDoc } from '../../db/index.js';
|
|
4
|
+
import type { IWebPushControllerMutation } from '../../webpush/classes.webpush-manager.js';
|
|
5
|
+
import {
|
|
6
|
+
requireGatewayMachineAuth,
|
|
7
|
+
type IGatewayMachineAuthContext,
|
|
8
|
+
} from '../helpers/gateway-client-auth.js';
|
|
9
|
+
|
|
10
|
+
type IWebPushResourceOwner = plugins.servezoneInterfaces.data.IWebPushResourceOwner;
|
|
11
|
+
|
|
12
|
+
export class WebPushControlHandler {
|
|
13
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
14
|
+
|
|
15
|
+
public constructor(private readonly opsServerRef: OpsServer) {
|
|
16
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
17
|
+
this.registerHandlers();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private registerHandlers(): void {
|
|
21
|
+
this.typedrouter.addTypedHandler(
|
|
22
|
+
new plugins.typedrequest.TypedHandler<
|
|
23
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_ListWebPushBindings
|
|
24
|
+
>(
|
|
25
|
+
'listWebPushBindings',
|
|
26
|
+
async (dataArg) => {
|
|
27
|
+
const auth = await this.requireControlAuth(dataArg.auth, 'readWebPush');
|
|
28
|
+
const owner = this.scopeOwnerFilter(auth, dataArg.owner);
|
|
29
|
+
const manager = this.opsServerRef.dcRouterRef.webPushManager;
|
|
30
|
+
return {
|
|
31
|
+
bindings: await manager.listBindings(owner),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
this.typedrouter.addTypedHandler(
|
|
38
|
+
new plugins.typedrequest.TypedHandler<
|
|
39
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_SyncWebPushBinding
|
|
40
|
+
>(
|
|
41
|
+
'syncWebPushBinding',
|
|
42
|
+
async (dataArg) => {
|
|
43
|
+
try {
|
|
44
|
+
const request = dataArg as typeof dataArg & {
|
|
45
|
+
controller: IWebPushControllerMutation;
|
|
46
|
+
};
|
|
47
|
+
const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush');
|
|
48
|
+
this.assertExactOwner(auth, dataArg.binding.owner);
|
|
49
|
+
const result = await this.opsServerRef.dcRouterRef.webPushManager.syncBinding(
|
|
50
|
+
dataArg.binding,
|
|
51
|
+
request.controller,
|
|
52
|
+
auth.userId,
|
|
53
|
+
);
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
binding: result.binding,
|
|
57
|
+
credential: result.credential,
|
|
58
|
+
};
|
|
59
|
+
} catch (error: unknown) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
message: this.safeMessage(error),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
this.typedrouter.addTypedHandler(
|
|
70
|
+
new plugins.typedrequest.TypedHandler<
|
|
71
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_DeleteWebPushBinding
|
|
72
|
+
>(
|
|
73
|
+
'deleteWebPushBinding',
|
|
74
|
+
async (dataArg) => {
|
|
75
|
+
try {
|
|
76
|
+
const request = dataArg as typeof dataArg & {
|
|
77
|
+
id?: string;
|
|
78
|
+
owner: IWebPushResourceOwner;
|
|
79
|
+
controller: IWebPushControllerMutation;
|
|
80
|
+
};
|
|
81
|
+
const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush');
|
|
82
|
+
this.assertExactOwner(auth, request.owner);
|
|
83
|
+
return {
|
|
84
|
+
success: await this.opsServerRef.dcRouterRef.webPushManager.deleteBinding({
|
|
85
|
+
...(request.id !== undefined ? { id: request.id } : {}),
|
|
86
|
+
owner: request.owner,
|
|
87
|
+
controller: request.controller,
|
|
88
|
+
}, auth.userId),
|
|
89
|
+
};
|
|
90
|
+
} catch (error: unknown) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
message: this.safeMessage(error),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
this.typedrouter.addTypedHandler(
|
|
101
|
+
new plugins.typedrequest.TypedHandler<
|
|
102
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_RotateWebPushCredential
|
|
103
|
+
>(
|
|
104
|
+
'rotateWebPushCredential',
|
|
105
|
+
async (dataArg) => {
|
|
106
|
+
try {
|
|
107
|
+
const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush');
|
|
108
|
+
const manager = this.opsServerRef.dcRouterRef.webPushManager;
|
|
109
|
+
const binding = await manager.getBindingByCredentialId(dataArg.credentialId);
|
|
110
|
+
if (!binding) {
|
|
111
|
+
throw new plugins.typedrequest.TypedResponseError('Web Push binding not found');
|
|
112
|
+
}
|
|
113
|
+
this.assertExactOwner(auth, this.bindingOwner(binding));
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
credential: await manager.rotateCredential(dataArg.credentialId),
|
|
117
|
+
};
|
|
118
|
+
} catch (error: unknown) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
message: this.safeMessage(error),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
this.typedrouter.addTypedHandler(
|
|
129
|
+
new plugins.typedrequest.TypedHandler<
|
|
130
|
+
plugins.servezoneInterfaces.requests.webpush.IReq_RotateWebPushVapidKey
|
|
131
|
+
>(
|
|
132
|
+
'rotateWebPushVapidKey',
|
|
133
|
+
async (dataArg) => {
|
|
134
|
+
try {
|
|
135
|
+
const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush');
|
|
136
|
+
const binding = await this.requireAuthorizedBinding(auth, dataArg.bindingId);
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
binding: await this.opsServerRef.dcRouterRef.webPushManager.rotateVapidKey(
|
|
140
|
+
binding.id,
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
} catch (error: unknown) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
message: this.safeMessage(error),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async requireControlAuth(
|
|
155
|
+
authArg: plugins.servezoneInterfaces.requests.webpush.IWebPushControlRequestAuth,
|
|
156
|
+
capabilityArg: 'readWebPush' | 'manageWebPush',
|
|
157
|
+
): Promise<IGatewayMachineAuthContext> {
|
|
158
|
+
const auth = await requireGatewayMachineAuth(
|
|
159
|
+
this.opsServerRef,
|
|
160
|
+
authArg,
|
|
161
|
+
capabilityArg === 'manageWebPush'
|
|
162
|
+
? 'gateway-clients:write'
|
|
163
|
+
: 'gateway-clients:read',
|
|
164
|
+
);
|
|
165
|
+
if (auth.isAdmin) return auth;
|
|
166
|
+
if (
|
|
167
|
+
auth.policy?.role !== 'gatewayClient'
|
|
168
|
+
|| auth.policy.capabilities?.[capabilityArg] !== true
|
|
169
|
+
) {
|
|
170
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
171
|
+
`token capability missing: ${capabilityArg}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return auth;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private scopeOwnerFilter(
|
|
178
|
+
authArg: IGatewayMachineAuthContext,
|
|
179
|
+
requestedArg: Partial<IWebPushResourceOwner> | undefined,
|
|
180
|
+
): Partial<IWebPushResourceOwner> {
|
|
181
|
+
if (authArg.isAdmin) return requestedArg || {};
|
|
182
|
+
const gatewayClient = authArg.gatewayClient;
|
|
183
|
+
if (!gatewayClient) {
|
|
184
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
185
|
+
'gateway-client credential required',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
requestedArg?.gatewayClientType !== undefined
|
|
190
|
+
&& requestedArg.gatewayClientType !== gatewayClient.type
|
|
191
|
+
) {
|
|
192
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
193
|
+
'gateway-client credential cannot access another Web Push owner',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
requestedArg?.gatewayClientId !== undefined
|
|
198
|
+
&& requestedArg.gatewayClientId !== gatewayClient.id
|
|
199
|
+
) {
|
|
200
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
201
|
+
'gateway-client credential cannot access another Web Push owner',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
gatewayClientType: gatewayClient.type,
|
|
206
|
+
gatewayClientId: gatewayClient.id,
|
|
207
|
+
...(requestedArg?.appInstanceId !== undefined
|
|
208
|
+
? { appInstanceId: requestedArg.appInstanceId }
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private assertExactOwner(
|
|
214
|
+
authArg: IGatewayMachineAuthContext,
|
|
215
|
+
ownerArg: IWebPushResourceOwner,
|
|
216
|
+
): void {
|
|
217
|
+
if (authArg.isAdmin) return;
|
|
218
|
+
const gatewayClient = authArg.gatewayClient;
|
|
219
|
+
if (
|
|
220
|
+
!gatewayClient
|
|
221
|
+
|| ownerArg.gatewayClientType !== gatewayClient.type
|
|
222
|
+
|| ownerArg.gatewayClientId !== gatewayClient.id
|
|
223
|
+
) {
|
|
224
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
225
|
+
'gateway-client credential cannot access another Web Push owner',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async requireAuthorizedBinding(
|
|
231
|
+
authArg: IGatewayMachineAuthContext,
|
|
232
|
+
bindingIdArg: string,
|
|
233
|
+
): Promise<WebPushBindingDoc> {
|
|
234
|
+
const binding = await this.opsServerRef.dcRouterRef.webPushManager.getBindingById(
|
|
235
|
+
bindingIdArg,
|
|
236
|
+
);
|
|
237
|
+
if (!binding) {
|
|
238
|
+
throw new plugins.typedrequest.TypedResponseError('Web Push binding not found');
|
|
239
|
+
}
|
|
240
|
+
this.assertExactOwner(authArg, this.bindingOwner(binding));
|
|
241
|
+
return binding;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private bindingOwner(bindingArg: WebPushBindingDoc): IWebPushResourceOwner {
|
|
245
|
+
return {
|
|
246
|
+
gatewayClientType: bindingArg.ownerGatewayClientType,
|
|
247
|
+
gatewayClientId: bindingArg.ownerGatewayClientId,
|
|
248
|
+
appInstanceId: bindingArg.ownerAppInstanceId,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private safeMessage(errorArg: unknown): string {
|
|
253
|
+
return errorArg instanceof plugins.typedrequest.TypedResponseError
|
|
254
|
+
? errorArg.message
|
|
255
|
+
: 'Web Push control operation failed';
|
|
256
|
+
}
|
|
257
|
+
}
|
package/ts/plugins.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as dns from 'node:dns';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
5
5
|
import * as http from 'node:http';
|
|
6
|
+
import * as https from 'node:https';
|
|
6
7
|
import * as net from 'node:net';
|
|
7
8
|
import * as buffer from 'node:buffer';
|
|
8
9
|
import * as os from 'node:os';
|
|
@@ -16,6 +17,7 @@ export {
|
|
|
16
17
|
fs,
|
|
17
18
|
crypto,
|
|
18
19
|
http,
|
|
20
|
+
https,
|
|
19
21
|
net,
|
|
20
22
|
buffer,
|
|
21
23
|
os,
|
|
@@ -99,8 +101,12 @@ export {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
// third party
|
|
104
|
+
import ipaddr from 'ipaddr.js';
|
|
102
105
|
import * as uuid from 'uuid';
|
|
106
|
+
import webpush from 'web-push';
|
|
103
107
|
|
|
104
108
|
export {
|
|
109
|
+
ipaddr,
|
|
105
110
|
uuid,
|
|
111
|
+
webpush,
|
|
106
112
|
}
|
|
@@ -21,6 +21,10 @@ export function buildEdgeOnlyDerivationRoutes(options: { dnsNsDomains?: string[]
|
|
|
21
21
|
type: 'forward',
|
|
22
22
|
targets: [{ host: '127.0.0.1', port: 53 }],
|
|
23
23
|
},
|
|
24
|
+
ingress: {
|
|
25
|
+
directHub: false,
|
|
26
|
+
smartVpn: false,
|
|
27
|
+
},
|
|
24
28
|
remoteIngress: {
|
|
25
29
|
enabled: true,
|
|
26
30
|
edgeFilter: [REMOTE_INGRESS_DNS_TAG],
|