add frontend

+1
.gitignore
···
.env
+
node_modules/
+4 -4
cmd/api/main.go
···
_ "github.com/lib/pq"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/api"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/config"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/database"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/logging"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/api"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/config"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/database"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/logging"
)
func main() {
+4 -4
cmd/mirror/main.go
···
"log"
"os"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/config"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/database"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/logging"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/mirror"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/config"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/database"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/logging"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/mirror"
)
func main() {
+3 -3
ent/client.go
···
"log"
"reflect"
-
"tangled.sh/seiso.moe/alethia.directory/ent/migrate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/migrate"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// Client is the client that holds all ent builders.
+2 -2
ent/ent.go
···
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// ent aliases to avoid import conflicts in user's code.
+3 -3
ent/enttest/enttest.go
···
import (
"context"
-
"tangled.sh/seiso.moe/alethia.directory/ent"
+
"tangled.sh/seiso.moe/aletheia.directory/ent"
// required by schema hooks.
-
_ "tangled.sh/seiso.moe/alethia.directory/ent/runtime"
+
_ "tangled.sh/seiso.moe/aletheia.directory/ent/runtime"
"entgo.io/ent/dialect/sql/schema"
-
"tangled.sh/seiso.moe/alethia.directory/ent/migrate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/migrate"
)
type (
+1 -1
ent/hook/hook.go
···
"context"
"fmt"
-
"tangled.sh/seiso.moe/alethia.directory/ent"
+
"tangled.sh/seiso.moe/aletheia.directory/ent"
)
// The OperationFunc type is an adapter to allow the use of ordinary
+4 -4
ent/mutation.go
···
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
const (
+2 -2
ent/operation.go
···
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
// Operation is the model entity for the Operation schema.
+1 -1
ent/operation/where.go
···
"time"
"entgo.io/ent/dialect/sql"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
)
// ID filters vertices based on their ID field.
+2 -2
ent/operation_create.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
// OperationCreate is the builder for creating a Operation entity.
+2 -2
ent/operation_delete.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
)
// OperationDelete is the builder for deleting a Operation entity.
+2 -2
ent/operation_query.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
)
// OperationQuery is the builder for querying Operation entities.
+3 -3
ent/operation_update.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
// OperationUpdate is the builder for updating Operation entities.
+2 -2
ent/runtime.go
···
import (
"time"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/schema"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/schema"
)
// The init function reads all schema descriptors with runtime code
+1 -1
ent/runtime/runtime.go
···
package runtime
-
// The schema-stitching logic is generated in tangled.sh/seiso.moe/alethia.directory/ent/runtime.go
+
// The schema-stitching logic is generated in tangled.sh/seiso.moe/aletheia.directory/ent/runtime.go
const (
Version = "v0.14.4" // Version of ent codegen.
+1 -1
ent/schema/operation.go
···
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
type Operation struct {
+1 -1
ent/syncstatus.go
···
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// SyncStatus is the model entity for the SyncStatus schema.
+1 -1
ent/syncstatus/where.go
···
"time"
"entgo.io/ent/dialect/sql"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
)
// ID filters vertices based on their ID field.
+1 -1
ent/syncstatus_create.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// SyncStatusCreate is the builder for creating a SyncStatus entity.
+2 -2
ent/syncstatus_delete.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// SyncStatusDelete is the builder for deleting a SyncStatus entity.
+2 -2
ent/syncstatus_query.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// SyncStatusQuery is the builder for querying SyncStatus entities.
+2 -2
ent/syncstatus_update.go
···
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
-
"tangled.sh/seiso.moe/alethia.directory/ent/predicate"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/predicate"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
)
// SyncStatusUpdate is the builder for updating SyncStatus entities.
+1 -1
go.mod
···
-
module tangled.sh/seiso.moe/alethia.directory
+
module tangled.sh/seiso.moe/aletheia.directory
go 1.24.4
+40 -10
pkg/api/server.go
···
_ "github.com/lib/pq"
did "github.com/whyrusleeping/go-did"
-
"tangled.sh/seiso.moe/alethia.directory/ent"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/ent/syncstatus"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/ent"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
var (
···
}
type VerificationMethod struct {
-
ID string
-
Type string
-
Controller string
-
PublicKeyMultibase string
+
ID string `json:"id"`
+
Type string `json:"type"`
+
Controller string `json:"controller"`
+
PublicKeyMultibase string `json:"publicKeyMultibase"`
+
}
+
+
type OperationResponse struct {
+
DID string `json:"did"`
+
Operation plc.PLCOperation `json:"operation"`
+
CID string `json:"cid"`
+
Nullified bool `json:"nullified"`
+
CreatedAt time.Time `json:"createdAt"`
}
type KeyAndContext struct {
···
return
}
-
s.writeJSONResponse(w, http.StatusOK, n)
+
responses := make([]OperationResponse, len(n))
+
for i, op := range n {
+
responses[i] = OperationResponse{
+
DID: op.Did,
+
Operation: op.Operation,
+
CID: op.Cid,
+
Nullified: op.Nullified,
+
CreatedAt: op.CreatedAt,
+
}
+
}
+
+
s.writeJSONResponse(w, http.StatusOK, responses)
}
func (s *Server) handleLastOp(w http.ResponseWriter, r *http.Request) {
···
return
}
-
s.writeJSONLResponse(w, http.StatusOK, ops)
+
responses := make([]OperationResponse, len(ops))
+
for i, op := range ops {
+
responses[i] = OperationResponse{
+
DID: op.Did,
+
Operation: op.Operation,
+
CID: op.Cid,
+
Nullified: op.Nullified,
+
CreatedAt: op.CreatedAt,
+
}
+
}
+
+
s.writeJSONLResponse(w, http.StatusOK, responses)
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
+1 -1
pkg/database/database.go
···
entsql "entgo.io/ent/dialect/sql"
_ "github.com/lib/pq"
-
"tangled.sh/seiso.moe/alethia.directory/ent"
+
"tangled.sh/seiso.moe/aletheia.directory/ent"
)
func NewClient(dbURL string, logger *slog.Logger) (*ent.Client, error) {
+3 -3
pkg/mirror/service.go
···
"net/http"
"time"
-
"tangled.sh/seiso.moe/alethia.directory/ent"
-
"tangled.sh/seiso.moe/alethia.directory/ent/operation"
-
"tangled.sh/seiso.moe/alethia.directory/pkg/plc"
+
"tangled.sh/seiso.moe/aletheia.directory/ent"
+
"tangled.sh/seiso.moe/aletheia.directory/ent/operation"
+
"tangled.sh/seiso.moe/aletheia.directory/pkg/plc"
)
type PLCExportEntry struct {
+23
web/.gitignore
···
+
node_modules
+
+
# Output
+
.output
+
.vercel
+
.netlify
+
.wrangler
+
/.svelte-kit
+
/build
+
+
# OS
+
.DS_Store
+
Thumbs.db
+
+
# Env
+
.env
+
.env.*
+
!.env.example
+
!.env.test
+
+
# Vite
+
vite.config.js.timestamp-*
+
vite.config.ts.timestamp-*
+1
web/.npmrc
···
+
engine-strict=true
+38
web/README.md
···
+
# sv
+
+
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
+
+
## Creating a project
+
+
If you're seeing this, you've probably already done this step. Congrats!
+
+
```bash
+
# create a new project in the current directory
+
npx sv create
+
+
# create a new project in my-app
+
npx sv create my-app
+
```
+
+
## Developing
+
+
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+
```bash
+
npm run dev
+
+
# or start the server and open the app in a new browser tab
+
npm run dev -- --open
+
```
+
+
## Building
+
+
To create a production version of your app:
+
+
```bash
+
npm run build
+
```
+
+
You can preview the production build with `npm run preview`.
+
+
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
web/bun.lockb

This is a binary file and will not be displayed.

+1
web/fonts/.gitignore
···
+
BerkeleyMono-Regular.woff2
+32
web/package.json
···
+
{
+
"name": "web",
+
"private": true,
+
"version": "0.0.1",
+
"type": "module",
+
"scripts": {
+
"dev": "vite dev",
+
"build": "vite build",
+
"preview": "vite preview",
+
"prepare": "svelte-kit sync || echo ''",
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
+
},
+
"devDependencies": {
+
"@sveltejs/adapter-auto": "^6.0.0",
+
"@sveltejs/kit": "^2.16.0",
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
+
"@tailwindcss/typography": "^0.5.16",
+
"autoprefixer": "^10.4.21",
+
"postcss": "^8.5.4",
+
"svelte": "^5.0.0",
+
"svelte-check": "^4.0.0",
+
"tailwindcss": "^4.1.8",
+
"typescript": "^5.0.0",
+
"vite": "^6.2.6"
+
},
+
"dependencies": {
+
"@atcute/did-plc": "^0.1.6",
+
"@atcute/identity-resolver": "^1.1.3",
+
"@tailwindcss/vite": "^4.1.8"
+
}
+
}
+8
web/src/app.css
···
+
@import "tailwindcss";
+
+
@font-face {
+
font-family: 'Berkeley Mono';
+
src: url('./fonts/BerkeleyMono-Regular.woff2') format('woff2');
+
font-weight: normal;
+
font-style: normal;
+
}
+13
web/src/app.d.ts
···
+
// See https://svelte.dev/docs/kit/types#app.d.ts
+
// for information about these interfaces
+
declare global {
+
namespace App {
+
// interface Error {}
+
// interface Locals {}
+
// interface PageData {}
+
// interface PageState {}
+
// interface Platform {}
+
}
+
}
+
+
export {};
+13
web/src/app.html
···
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="utf-8" />
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
+
<title>PLC Directory</title>
+
%sveltekit.head%
+
</head>
+
<body data-sveltekit-preload-data="hover">
+
<div style="display: contents">%sveltekit.body%</div>
+
</body>
+
</html>
+104
web/src/components/AuditLog.svelte
···
+
<script lang="ts">
+
export let auditData: { canonical: any[], nullified: any[] } | null = null;
+
+
function formatTimestamp(timestamp: string | number | Date) {
+
try {
+
return new Date(timestamp).toLocaleString();
+
} catch {
+
return 'Invalid date';
+
}
+
}
+
+
function getOperationType(operation: any) {
+
if (operation.operation) return operation.operation.type;
+
if (operation.type) return operation.type;
+
return 'Unknown';
+
}
+
</script>
+
+
{#if auditData}
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors">
+
<h2 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">Audit Log</h2>
+
+
<div class="space-y-4">
+
{#if auditData.canonical && auditData.canonical.length > 0}
+
<div>
+
<h3 class="text-lg font-medium mb-3 text-gray-800 dark:text-gray-200">Valid Operations</h3>
+
<div class="space-y-2">
+
{#each auditData.canonical.slice().reverse() as operation, index}
+
<div class="flex items-center justify-between p-3
+
bg-green-50 dark:bg-green-900/20
+
border border-green-200 dark:border-green-800
+
rounded-md transition-colors">
+
<div class="flex-1">
+
<div class="flex items-center gap-3">
+
<span class="font-medium text-gray-900 dark:text-white">
+
Operation #{auditData.canonical.length - index}
+
</span>
+
<span class="text-sm text-gray-600 dark:text-gray-400">
+
{getOperationType(operation)}
+
</span>
+
</div>
+
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400">
+
{#if operation.createdAt}
+
<div>Created At: {formatTimestamp(operation.createdAt)}</div>
+
{/if}
+
<div class="font-mono break-all">CID: {operation.cid}</div>
+
</div>
+
</div>
+
<div class="text-xs bg-green-100 dark:bg-green-800/30
+
text-green-700 dark:text-green-300
+
px-2 py-1 rounded transition-colors">
+
VALID
+
</div>
+
</div>
+
{/each}
+
</div>
+
</div>
+
{/if}
+
+
{#if auditData.nullified && auditData.nullified.length > 0}
+
<div>
+
<h3 class="text-lg font-medium mb-3 text-gray-800 dark:text-gray-200">Nullified Operations</h3>
+
<div class="space-y-2">
+
{#each auditData.canonical.slice().reverse() as operation, index}
+
<div class="flex items-center justify-between p-3
+
bg-purple-50 dark:bg-purple-900/20
+
border border-purple-200 dark:border-purple-800
+
rounded-md transition-colors">
+
<div class="flex-1">
+
<div class="flex items-center gap-3">
+
<div class="w-3 h-3 bg-purple-500 rounded-full"></div>
+
<span class="font-medium text-gray-900 dark:text-white">
+
Nullified #{auditData.canonical.length - index}
+
</span>
+
<span class="text-sm text-gray-600 dark:text-gray-400">
+
{getOperationType(operation)}
+
</span>
+
</div>
+
<div class="mt-1 ml-6 text-sm text-gray-600 dark:text-gray-400">
+
{#if operation.createdAt}
+
<div>Created At: {formatTimestamp(operation.createdAt)}</div>
+
{/if}
+
<div class="font-mono break-all">CID: {operation.cid}</div>
+
</div>
+
</div>
+
<div class="text-xs bg-purple-100 dark:bg-purple-800/30
+
text-purple-700 dark:text-purple-300
+
px-2 py-1 rounded transition-colors">
+
NULLIFIED
+
</div>
+
</div>
+
{/each}
+
</div>
+
</div>
+
{/if}
+
+
{#if (!auditData.canonical || auditData.canonical.length === 0) && (!auditData.nullified || auditData.nullified.length === 0)}
+
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
+
<p>No audit log entries found</p>
+
</div>
+
{/if}
+
</div>
+
</div>
+
{/if}
+60
web/src/lib/api.ts
···
+
import { defs } from "@atcute/did-plc";
+
+
const API_BASE = "https://api.aletheia.directory";
+
+
export interface DidDocument {
+
"@context": string[];
+
id: string;
+
alsoKnownAs: string[];
+
verificationMethod: any[];
+
service: any[];
+
}
+
+
export class PLCDirectoryAPI {
+
private baseURL: string;
+
+
constructor(baseURL: string = API_BASE) {
+
this.baseURL = baseURL;
+
}
+
+
async fetchDidDocument(did: string): Promise<DidDocument> {
+
const response = await fetch(`${this.baseURL}/${did}`, {
+
mode: 'cors',
+
headers: {
+
'Content-Type': 'application/json',
+
}
+
});
+
if (!response.ok) {
+
const error = await response
+
.json()
+
.catch(() => ({ message: "Unknown error" }));
+
throw new Error(error.message || `HTTP ${response.status}`);
+
}
+
return response.json();
+
}
+
+
async fetchDidAuditLog(did: string): Promise<defs.IndexedEntryLog> {
+
const response = await fetch(`${this.baseURL}/${did}/log/audit`, {
+
mode: 'cors',
+
headers: {
+
'Content-Type': 'application/json',
+
}
+
});
+
if (!response.ok) {
+
const error = await response
+
.json()
+
.catch(() => ({ message: "Unknown error" }));
+
throw new Error(error.message || `HTTP ${response.status}`);
+
}
+
const data = await response.json();
+
const result = defs.indexedEntryLog.try(data);
+
if (!result.ok) {
+
console.log(result);
+
throw new Error(`Invalid audit log format: ${result.error.message}`);
+
}
+
+
return result.value;
+
}
+
}
+
+
export const api = new PLCDirectoryAPI();
+1
web/src/lib/index.ts
···
+
// place files you want to import through the `$lib` alias in this folder.
+10
web/src/routes/+layout.svelte
···
+
<script>
+
import { onMount } from 'svelte';
+
import { browser } from '$app/environment';
+
import '../app.css';
+
import 'tailwindcss';
+
+
let { children } = $props();
+
</script>
+
+
{@render children()}
+139
web/src/routes/+page.svelte
···
+
<script lang="ts">
+
import { api } from '$lib/api';
+
import { processIndexedEntryLog } from '@atcute/did-plc';
+
import {
+
CompositeHandleResolver,
+
DohJsonHandleResolver,
+
WellKnownHandleResolver,
+
DidNotFoundError,
+
InvalidResolvedHandleError,
+
AmbiguousHandleError,
+
FailedHandleResolutionError,
+
HandleResolutionError
+
} from '@atcute/identity-resolver';
+
import AuditLog from '../components/AuditLog.svelte';
+
+
let didInput = '';
+
let loading = false;
+
let result: any = null;
+
let auditLogData: { canonical: any[], nullified: any[] } | null = null;
+
let error = '';
+
let resolvedDid = '';
+
+
const handleResolver = new CompositeHandleResolver({
+
strategy: 'race',
+
methods: {
+
dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }),
+
http: new WellKnownHandleResolver(),
+
},
+
});
+
+
function isDid(input: string): boolean {
+
return input.startsWith('did:plc:');
+
}
+
+
async function resolveInputToDid(input: string): Promise<string> {
+
const trimmedInput = input.trim();
+
+
if (isDid(trimmedInput)) {
+
return trimmedInput;
+
}
+
+
try {
+
const resolvedHandle = await handleResolver.resolve(trimmedInput);
+
return resolvedHandle;
+
} catch (err) {
+
if (err instanceof DidNotFoundError) {
+
throw new Error("Handle not found - DID not found for this handle");
+
}
+
if (err instanceof InvalidResolvedHandleError) {
+
throw new Error("Invalid handle format");
+
}
+
if (err instanceof AmbiguousHandleError) {
+
throw new Error("Ambiguous handle - multiple DIDs found");
+
}
+
if (err instanceof FailedHandleResolutionError) {
+
throw new Error("Failed to resolve handle");
+
}
+
if (err instanceof HandleResolutionError) {
+
throw new Error("Error resolving handle");
+
}
+
throw new Error(`Handle resolution failed: ${err.message}`);
+
}
+
}
+
+
async function searchDID() {
+
if (!didInput.trim()) return;
+
+
loading = true;
+
error = '';
+
result = null;
+
auditLogData = null;
+
resolvedDid = '';
+
+
try {
+
const did = await resolveInputToDid(didInput);
+
resolvedDid = did;
+
+
const log = await api.fetchDidAuditLog(resolvedDid);
+
auditLogData = await processIndexedEntryLog(resolvedDid, log);
+
+
result = await api.fetchDidDocument(resolvedDid);
+
} catch (err: any) {
+
error = err.message;
+
} finally {
+
loading = false;
+
}
+
}
+
</script>
+
+
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
+
<div class="max-w-4xl mx-auto">
+
<div class="flex justify-between items-center mb-8">
+
<h1 class="text-4xl font-bold text-gray-900 dark:text-white text-center flex-1">
+
PLC Directory
+
</h1>
+
</div>
+
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6 transition-colors">
+
<div class="flex gap-4">
+
<input
+
bind:value={didInput}
+
type="text"
+
placeholder="Enter DID (did:plc:...) or handle (user.bsky.social)"
+
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600
+
bg-white dark:bg-gray-700 text-gray-900 dark:text-white
+
rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500
+
placeholder-gray-500 dark:placeholder-gray-400 transition-colors"
+
disabled={loading}
+
/>
+
<button
+
on:click={searchDID}
+
disabled={loading || !didInput.trim()}
+
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600
+
text-white rounded-md disabled:opacity-50 transition-colors"
+
>
+
{loading ? 'Searching...' : 'Search'}
+
</button>
+
</div>
+
+
{#if error}
+
<div class="mt-4 p-4 bg-red-50 dark:bg-red-900/50 border border-red-200 dark:border-red-800 rounded-md transition-colors">
+
<p class="text-red-800 dark:text-red-200">{error}</p>
+
</div>
+
{/if}
+
</div>
+
+
<div class="mb-6">
+
<AuditLog auditData={auditLogData} />
+
</div>
+
+
{#if result}
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors">
+
<h2 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">DID Document</h2>
+
<pre class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100
+
p-4 rounded-md overflow-x-auto text-sm transition-colors"><code>{JSON.stringify(result, null, 2)}</code></pre>
+
</div>
+
{/if}
+
</div>
+
</div>
web/static/favicon.png

This is a binary file and will not be displayed.

+18
web/svelte.config.js
···
+
import adapter from '@sveltejs/adapter-auto';
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+
/** @type {import('@sveltejs/kit').Config} */
+
const config = {
+
// Consult https://svelte.dev/docs/kit/integrations
+
// for more information about preprocessors
+
preprocess: vitePreprocess(),
+
+
kit: {
+
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
+
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
+
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
+
adapter: adapter()
+
}
+
};
+
+
export default config;
+12
web/tailwind.config.js
···
+
export default {
+
content: ['./src/**/*.{html,js,svelte,ts}'],
+
darkMode: 'class',
+
theme: {
+
extend: {
+
fontFamily: {
+
mono: ['Berkeley Mono', 'ui-monospace', 'SFMono-Regular', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace']
+
}
+
}
+
},
+
plugins: []
+
};
+19
web/tsconfig.json
···
+
{
+
"extends": "./.svelte-kit/tsconfig.json",
+
"compilerOptions": {
+
"allowJs": true,
+
"checkJs": true,
+
"esModuleInterop": true,
+
"forceConsistentCasingInFileNames": true,
+
"resolveJsonModule": true,
+
"skipLibCheck": true,
+
"sourceMap": true,
+
"strict": true,
+
"moduleResolution": "bundler"
+
}
+
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
+
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
+
//
+
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+
// from the referenced tsconfig.json - TypeScript does not merge them in
+
}
+10
web/vite.config.ts
···
+
import { sveltekit } from '@sveltejs/kit/vite';
+
import { defineConfig } from 'vite';
+
import tailwindcss from '@tailwindcss/vite';
+
+
export default defineConfig({
+
plugins: [
+
tailwindcss(),
+
sveltekit(),
+
]
+
});