Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.

modal work

+260 -109
+5 -2
apps/hosting-service/src/lib/file-serving.ts
··· 25 25 const result = await storage.getWithMetadata(key); 26 26 27 27 if (result) { 28 - const tier = result.metadata?.tier || 'unknown'; 28 + const metadata = result.metadata; 29 + const tier = 30 + metadata && typeof metadata === 'object' && 'tier' in metadata 31 + ? String((metadata as Record<string, unknown>).tier) 32 + : 'unknown'; 29 33 const size = result.data ? (result.data as Uint8Array).length : 0; 30 34 console.log(`[Storage] Served ${filePath} from ${tier} tier (${size} bytes) - ${did}:${rkey}`); 31 35 } ··· 833 837 }, 834 838 }); 835 839 } 836 -
+213 -99
apps/main-app/public/editor/tabs/DomainsTab.tsx
··· 76 76 const [customDomain, setCustomDomain] = useState('') 77 77 const [isAddingDomain, setIsAddingDomain] = useState(false) 78 78 const [viewDomainDNS, setViewDomainDNS] = useState<string | null>(null) 79 + const [copiedField, setCopiedField] = useState<string | null>(null) 80 + 81 + const copyToClipboard = async (value: string, label: string) => { 82 + try { 83 + await navigator.clipboard.writeText(value) 84 + setCopiedField(label) 85 + window.setTimeout(() => { 86 + setCopiedField((current) => (current === label ? null : current)) 87 + }, 1400) 88 + } catch { 89 + setCopiedField(null) 90 + } 91 + } 79 92 80 93 const checkWispAvailability = async (handle: string) => { 81 94 const trimmedHandle = handle.trim().toLowerCase() ··· 463 476 open={viewDomainDNS !== null} 464 477 onOpenChange={(open) => !open && setViewDomainDNS(null)} 465 478 > 466 - <DialogContent className="sm:max-w-lg"> 479 + <DialogContent className="sm:max-w-lg max-h-[80vh] overflow-hidden"> 467 480 <DialogHeader> 468 481 <DialogTitle>DNS Configuration</DialogTitle> 469 482 <DialogDescription> ··· 471 484 </DialogDescription> 472 485 </DialogHeader> 473 486 {viewDomainDNS && userInfo && ( 474 - <> 487 + <div className="relative max-h-[62vh] overflow-y-auto pr-2"> 488 + <div className="pointer-events-none sticky top-0 z-10 h-3 bg-gradient-to-b from-background to-transparent" /> 475 489 {(() => { 476 490 const domain = customDomains.find( 477 491 (d) => d.id === viewDomainDNS ··· 481 495 return ( 482 496 <div className="space-y-4 py-4"> 483 497 <div className="p-3 bg-muted/30 rounded-lg"> 484 - <p className="text-sm font-medium mb-1"> 485 - Domain: 498 + <p className="text-xs uppercase tracking-wide text-muted-foreground"> 499 + Domain 486 500 </p> 487 - <p className="font-mono text-sm"> 501 + <p className="font-mono text-sm mt-1"> 488 502 {domain.domain} 489 503 </p> 490 504 </div> 491 505 492 506 <div className="space-y-3"> 493 - <div className="p-3 bg-background rounded border border-border"> 494 - <div className="flex justify-between items-start mb-2"> 495 - <span className="text-xs font-semibold text-muted-foreground"> 496 - TXT Record (Verification) 497 - </span> 498 - </div> 499 - <div className="font-mono text-xs space-y-2"> 507 + <div className="p-4 bg-background rounded border border-border"> 508 + <div className="flex items-center justify-between gap-3"> 500 509 <div> 501 - <span className="text-muted-foreground"> 502 - Name: 503 - </span>{' '} 504 - <span className="select-all"> 505 - _wisp.{domain.domain} 506 - </span> 510 + <p className="text-xs uppercase tracking-wide text-muted-foreground"> 511 + Step 1 512 + </p> 513 + <p className="text-sm font-semibold"> 514 + Verify ownership (TXT) 515 + </p> 507 516 </div> 508 - <div> 509 - <span className="text-muted-foreground"> 510 - Value: 511 - </span>{' '} 512 - <span className="select-all break-all"> 513 - {userInfo.did} 514 - </span> 517 + <Badge variant="secondary" className="text-xs"> 518 + Required 519 + </Badge> 520 + </div> 521 + <div className="mt-3 space-y-2"> 522 + <div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2"> 523 + <div className="min-w-0"> 524 + <p className="text-xs text-muted-foreground"> 525 + Name 526 + </p> 527 + <p className="font-mono text-sm select-all"> 528 + _wisp.{domain.domain} 529 + </p> 530 + </div> 531 + <Button 532 + variant="outline" 533 + size="sm" 534 + onClick={() => 535 + copyToClipboard( 536 + `_wisp.${domain.domain}`, 537 + 'txt-name' 538 + ) 539 + } 540 + > 541 + {copiedField === 'txt-name' 542 + ? 'Copied' 543 + : 'Copy'} 544 + </Button> 545 + </div> 546 + <div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2"> 547 + <div className="min-w-0"> 548 + <p className="text-xs text-muted-foreground"> 549 + Value 550 + </p> 551 + <p className="font-mono text-sm break-all select-all"> 552 + {userInfo.did} 553 + </p> 554 + </div> 555 + <Button 556 + variant="outline" 557 + size="sm" 558 + onClick={() => 559 + copyToClipboard(userInfo.did, 'txt-value') 560 + } 561 + > 562 + {copiedField === 'txt-value' 563 + ? 'Copied' 564 + : 'Copy'} 565 + </Button> 515 566 </div> 516 567 </div> 517 568 </div> 518 569 519 - <div className="p-3 bg-background rounded border border-border"> 520 - <div className="flex justify-between items-start mb-2"> 521 - <span className="text-xs font-semibold text-muted-foreground"> 522 - CNAME Record (Pointing) — Recommended 523 - </span> 570 + <div className="p-4 bg-background rounded border border-border"> 571 + <div className="flex items-center justify-between gap-3"> 572 + <div> 573 + <p className="text-xs uppercase tracking-wide text-muted-foreground"> 574 + Step 2 575 + </p> 576 + <p className="text-sm font-semibold"> 577 + Point your domain (CNAME) 578 + </p> 579 + </div> 580 + <Badge variant="secondary" className="text-xs"> 581 + Recommended 582 + </Badge> 524 583 </div> 525 - <div className="font-mono text-xs space-y-2"> 526 - <div> 527 - <span className="text-muted-foreground"> 528 - Name: 529 - </span>{' '} 530 - <span className="select-all"> 531 - {domain.domain} 532 - </span> 584 + <div className="mt-3 space-y-2"> 585 + <div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2"> 586 + <div className="min-w-0"> 587 + <p className="text-xs text-muted-foreground"> 588 + Name 589 + </p> 590 + <p className="font-mono text-sm select-all"> 591 + {domain.domain} 592 + </p> 593 + </div> 594 + <Button 595 + variant="outline" 596 + size="sm" 597 + onClick={() => 598 + copyToClipboard(domain.domain, 'cname-name') 599 + } 600 + > 601 + {copiedField === 'cname-name' 602 + ? 'Copied' 603 + : 'Copy'} 604 + </Button> 533 605 </div> 534 - <div> 535 - <span className="text-muted-foreground"> 536 - Value: 537 - </span>{' '} 538 - <span className="select-all"> 539 - {domain.id}.dns.wisp.place 540 - </span> 606 + <div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2"> 607 + <div className="min-w-0"> 608 + <p className="text-xs text-muted-foreground"> 609 + Value 610 + </p> 611 + <p className="font-mono text-sm select-all"> 612 + {domain.id}.dns.wisp.place 613 + </p> 614 + </div> 615 + <Button 616 + variant="outline" 617 + size="sm" 618 + onClick={() => 619 + copyToClipboard( 620 + `${domain.id}.dns.wisp.place`, 621 + 'cname-value' 622 + ) 623 + } 624 + > 625 + {copiedField === 'cname-value' 626 + ? 'Copied' 627 + : 'Copy'} 628 + </Button> 541 629 </div> 542 630 </div> 543 - <p className="text-xs text-muted-foreground mt-2"> 544 - Note: Some DNS providers (like Cloudflare) flatten CNAMEs to A records - this is fine and won't affect verification. 545 - </p> 631 + <div className="mt-3 flex gap-2 rounded-md border border-blue-500/20 bg-blue-500/10 p-2 text-xs text-blue-200"> 632 + <AlertCircle className="w-4 h-4 shrink-0 mt-0.5 text-blue-300" /> 633 + <p> 634 + Some DNS providers (like Cloudflare) flatten CNAMEs to A records. 635 + That&apos;s okay and won&apos;t affect verification. 636 + </p> 637 + </div> 546 638 </div> 547 639 548 - <div className="p-3 bg-background rounded border border-border"> 549 - <div className="flex items-start gap-2 mb-2"> 550 - <span className="text-xs font-semibold text-muted-foreground"> 551 - A Records (Fallback Option) 552 - </span> 553 - </div> 554 - <div className="p-2 bg-yellow-500/10 border border-yellow-500/20 rounded mb-3 flex gap-2"> 555 - <AlertCircle className="w-4 h-4 text-yellow-600 shrink-0 mt-0.5" /> 556 - <p className="text-xs text-yellow-700 dark:text-yellow-500"> 557 - <strong>Warning:</strong> Using A records instead of CNAME means you lose GeoDNS capabilities. 558 - Your site will always be served from the specific node you choose below, regardless of visitor location. 559 - </p> 560 - </div> 561 - <div className="space-y-3"> 562 - {HOSTING_NODES.map((node) => ( 563 - <div key={node.ip} className="font-mono text-xs space-y-1 pl-3 border-l-2 border-muted"> 564 - <div className="font-semibold text-muted-foreground mb-1"> 565 - {node.region} 566 - </div> 567 - <div> 568 - <span className="text-muted-foreground"> 569 - Name: 570 - </span>{' '} 571 - <span className="select-all"> 572 - {domain.domain} 573 - </span> 574 - </div> 575 - <div> 576 - <span className="text-muted-foreground"> 577 - Type: 578 - </span>{' '} 579 - <span>A</span> 580 - </div> 581 - <div> 582 - <span className="text-muted-foreground"> 583 - Value: 584 - </span>{' '} 585 - <span className="select-all"> 586 - {node.ip} 587 - </span> 640 + <details className="p-4 bg-background rounded border border-border"> 641 + <summary className="text-sm font-semibold cursor-pointer select-none"> 642 + Use A Records Instead (Fallback) 643 + </summary> 644 + <div className="mt-3"> 645 + <div className="p-2 bg-yellow-500/10 border border-yellow-500/20 rounded mb-3 flex gap-2"> 646 + <AlertCircle className="w-4 h-4 text-yellow-600 shrink-0 mt-0.5" /> 647 + <p className="text-sm text-yellow-700 dark:text-yellow-500"> 648 + <strong>Warning:</strong> A records disable GeoDNS. Your site 649 + will always be served from the single region you choose. 650 + </p> 651 + </div> 652 + <div className="space-y-3"> 653 + {HOSTING_NODES.map((node) => ( 654 + <div key={node.ip} className="space-y-2 pl-3 border-l-2 border-muted"> 655 + <div className="font-semibold text-muted-foreground mb-1"> 656 + {node.region} 657 + </div> 658 + <div className="font-mono text-xs space-y-1"> 659 + <div> 660 + <span className="text-muted-foreground"> 661 + Name: 662 + </span>{' '} 663 + <span className="select-all"> 664 + {domain.domain} 665 + </span> 666 + </div> 667 + <div> 668 + <span className="text-muted-foreground"> 669 + Type: 670 + </span>{' '} 671 + <span>A</span> 672 + </div> 673 + </div> 674 + <div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2 font-mono text-xs"> 675 + <div className="min-w-0"> 676 + <p className="text-xs text-muted-foreground"> 677 + Value 678 + </p> 679 + <p className="select-all"> 680 + {node.ip} 681 + </p> 682 + </div> 683 + <Button 684 + variant="outline" 685 + size="sm" 686 + onClick={() => 687 + copyToClipboard( 688 + node.ip, 689 + `a-value-${node.ip}` 690 + ) 691 + } 692 + > 693 + {copiedField === `a-value-${node.ip}` 694 + ? 'Copied' 695 + : 'Copy'} 696 + </Button> 697 + </div> 588 698 </div> 589 - </div> 590 - ))} 699 + ))} 700 + </div> 701 + <p className="text-sm text-muted-foreground mt-3"> 702 + Choose the region closest to your primary audience. 703 + </p> 591 704 </div> 592 - <p className="text-xs text-muted-foreground mt-3"> 593 - Choose one region that best matches your primary audience location. 594 - </p> 595 - </div> 705 + </details> 596 706 </div> 597 707 598 708 <div className="p-3 bg-muted/30 rounded-lg"> 599 - <p className="text-xs text-muted-foreground"> 600 - 💡 After configuring DNS, click "Verify DNS" 601 - to check if everything is set up correctly. 602 - DNS changes can take a few minutes to 709 + <p className="text-sm text-muted-foreground"> 710 + After configuring DNS, click "Verify DNS" to check 711 + everything. DNS changes can take a few minutes to 603 712 propagate. 604 713 </p> 605 714 </div> 606 715 </div> 607 716 ) 608 717 })()} 609 - </> 718 + <div className="pointer-events-none sticky bottom-0 z-10 flex h-8 items-end justify-center bg-gradient-to-t from-background to-transparent"> 719 + <span className="text-[10px] text-muted-foreground"> 720 + Scroll for more 721 + </span> 722 + </div> 723 + </div> 610 724 )} 611 725 <DialogFooter> 612 726 <Button
+22 -1
bun.lock
··· 153 153 "dependencies": { 154 154 "@opentelemetry/api": "^1.9.0", 155 155 "@opentelemetry/exporter-metrics-otlp-http": "^0.56.0", 156 + "@opentelemetry/otlp-exporter-base": "^0.211.0", 156 157 "@opentelemetry/resources": "^1.29.0", 157 158 "@opentelemetry/sdk-metrics": "^1.29.0", 158 159 "@opentelemetry/semantic-conventions": "^1.29.0", ··· 163 164 "typescript": "^5.9.3", 164 165 }, 165 166 "peerDependencies": { 167 + "elysia": "^1.0.0", 166 168 "hono": "^4.10.7", 167 169 }, 168 170 "optionalPeers": [ 171 + "elysia", 169 172 "hono", 170 173 ], 171 174 }, ··· 481 484 482 485 "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="], 483 486 484 - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="], 487 + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="], 485 488 486 489 "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], 487 490 ··· 1361 1364 1362 1365 "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1363 1366 1367 + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="], 1368 + 1364 1369 "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="], 1365 1370 1366 1371 "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="], ··· 1410 1415 "@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1411 1416 1412 1417 "@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1418 + 1419 + "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], 1420 + 1421 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-logs": "0.211.0", "@opentelemetry/sdk-metrics": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA=="], 1413 1422 1414 1423 "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1415 1424 ··· 1540 1549 "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1541 1550 1542 1551 "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1552 + 1553 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], 1554 + 1555 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], 1556 + 1557 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA=="], 1558 + 1559 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA=="], 1560 + 1561 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], 1562 + 1563 + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@8.0.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw=="], 1543 1564 1544 1565 "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1545 1566
+2 -1
packages/@wisp/observability/package.json
··· 37 37 }, 38 38 "dependencies": { 39 39 "@opentelemetry/api": "^1.9.0", 40 - "@opentelemetry/sdk-metrics": "^1.29.0", 41 40 "@opentelemetry/exporter-metrics-otlp-http": "^0.56.0", 41 + "@opentelemetry/otlp-exporter-base": "^0.211.0", 42 42 "@opentelemetry/resources": "^1.29.0", 43 + "@opentelemetry/sdk-metrics": "^1.29.0", 43 44 "@opentelemetry/semantic-conventions": "^1.29.0" 44 45 }, 45 46 "devDependencies": {
+3 -2
packages/@wisp/observability/src/exporters.ts
··· 7 7 import { metrics, type MeterProvider, type Counter, type Histogram } from '@opentelemetry/api' 8 8 import { MeterProvider as SdkMeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' 9 9 import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http' 10 + import type { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base' 10 11 import { Resource } from '@opentelemetry/resources' 11 12 import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions' 12 13 import os from 'node:os' ··· 326 327 url: `${this.config.prometheusUrl}${prometheusPath}`, 327 328 headers: this.getAuthHeaders(), 328 329 timeoutMillis: 10000, 329 - compression: 'gzip' 330 + compression: 'gzip' as CompressionAlgorithm 330 331 }) 331 332 332 333 // Create meter provider with periodic exporting ··· 454 455 if (typeof process !== 'undefined') { 455 456 process.on('SIGTERM', shutdownGrafanaExporters) 456 457 process.on('SIGINT', shutdownGrafanaExporters) 457 - } 458 + }
+15 -4
packages/@wisp/observability/src/middleware/elysia.ts
··· 6 6 * Tracks request metrics and logs errors 7 7 */ 8 8 export function observabilityMiddleware(service: string) { 9 + const normalizeStatus = (status: unknown, fallback: number) => { 10 + if (typeof status === 'number') return status 11 + if (typeof status === 'string') { 12 + const parsed = Number(status) 13 + if (!Number.isNaN(parsed)) return parsed 14 + } 15 + return fallback 16 + } 17 + 9 18 return { 10 19 beforeHandle: ({ request }: Context) => { 11 20 // Store start time on request object ··· 15 24 const startTime = (request as any).__startTime || Date.now() 16 25 const duration = Date.now() - startTime 17 26 const url = new URL(request.url) 27 + const statusCode = normalizeStatus(set.status, 200) 18 28 19 29 metricsCollector.recordRequest( 20 30 url.pathname, 21 31 request.method, 22 - set.status || 200, 32 + statusCode, 23 33 duration, 24 34 service 25 35 ) 26 36 }, 27 - onError: ({ request, error, set }: Context & { error: Error }) => { 37 + onError: (context: any) => { 38 + const { request, error, set } = context as Context & { error: Error } 28 39 const startTime = (request as any).__startTime || Date.now() 29 40 const duration = Date.now() - startTime 30 41 const url = new URL(request.url) 42 + const statusCode = normalizeStatus(set.status, 500) 31 43 32 44 metricsCollector.recordRequest( 33 45 url.pathname, 34 46 request.method, 35 - set.status || 500, 47 + statusCode, 36 48 duration, 37 49 service 38 50 ) 39 51 40 52 // Don't log 404 errors 41 - const statusCode = set.status || 500 42 53 if (statusCode !== 404) { 43 54 logCollector.error( 44 55 `Request failed: ${request.method} ${url.pathname}`,