Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions docs/docs/Post Platform Guide/platform-evault-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const { w3id, uri } = data;

**Request fields**


| Field | Description |
| --- | --- |
| `registryEntropy` | The entropy token from Step 1. |
Expand All @@ -84,9 +85,62 @@ const { w3id, uri } = data;
| `w3id` | The platform's assigned W3ID / eName. |
| `uri` | The endpoint URI of the newly provisioned eVault. |

### Step 3 — Persist the mapping
### Step 3 — Write the PlatformProfile into the eVault

With the eVault provisioned, write a **PlatformProfile** MetaEnvelope into it. This is the record other participants read to discover your platform — the **Marketplace**, for example, pulls every PlatformProfile from **Awareness-as-a-Service** and renders one card per platform. Its `url`, `logoUrl`, and `category` fields are what drive that card, so fill them in.

The profile is stored under the **User-profile ontology** (`550e8400-e29b-41d4-a716-446655440000`) with a public ACL (`["*"]`). It is distinguished from an ordinary user profile by the presence of a `platformName` field — that is exactly the marker consumers filter on.

Save `w3id` and `uri` locally so the platform can resolve and reuse its eVault on every subsequent boot, and so the existence check in Step 0 short-circuits. Cerberus stores this under the fixed key `cerberus-platform`.
```ts
const now = new Date().toISOString();
const platformProfile = {
platformName: "cerberus", // stable slug; the discovery marker
displayName: "Cerberus Platform",
description: "Secure messaging and group management platform",
version: "1.0.0",
ename: w3id,
isActive: true,
isArchived: false,
createdAt: now,
updatedAt: now,
url: "https://cerberus.w3ds.metastate.foundation", // public web app URL
logoUrl: "https://cerberus.w3ds.metastate.foundation/logo.png", // absolute logo URL
category: "Social", // Identity | Social | Governance | Wellness | Finance | Storage | Productivity
};

// storeMetaEnvelope only needs the X-ENAME header (no Bearer token).
await client.request(STORE_META_ENVELOPE, {
input: {
ontology: "550e8400-e29b-41d4-a716-446655440000",
payload: platformProfile,
acl: ["*"],
},
});
```

**Profile fields**

| Field | Type | Purpose |
| --- | --- | --- |
| `platformName` | string | Stable machine slug and the marker consumers filter on to tell a platform profile apart from a user profile. |
| `displayName` | string | Human-readable name shown to users. |
| `description` | string | Short blurb describing the platform. |
| `version` | string | Platform profile version. |
| `ename` | string | The platform's own eName (`w3id` from Step 2). |
| `isActive` | boolean | Whether the platform is live; consumers hide `false`. |
| `isArchived` | boolean | Soft-delete flag; consumers hide `true`. |
| `createdAt` / `updatedAt` | string | ISO-8601 timestamps. |
| `url` | string | **Public web app URL** — where "Open App" links. Required for the platform to be launchable from the Marketplace. |
| `logoUrl` | string | **Absolute URL** to the platform logo. Leave empty (`""`) to fall back to a placeholder icon. |
| `category` | string | One of `Identity`, `Social`, `Governance`, `Wellness`, `Finance`, `Storage`, `Productivity` (or a custom value — it becomes a filter chip). |

:::note
`storeMetaEnvelope` creates a **new** MetaEnvelope each call. To later change a field (e.g. add a `url` that predates this schema), update the existing profile by id with `updateMetaEnvelope(id, …)` — which requires a Bearer token — rather than calling `storeMetaEnvelope` again, otherwise you create a duplicate profile.
:::

### Step 4 — Persist the mapping

Save `w3id` and `uri` locally so the platform can resolve and reuse its eVault on every subsequent boot, and so the existence check in Step 0 short-circuits. Cerberus stores this under the fixed key `cerberus-platform`. Mirror the same `url`/`logoUrl`/`category` into the locally cached profile data so a later `updatePlatformProfile()` doesn't drop them.

Once persisted, helpers such as `getPlatformEName()` and `getPlatformEVaultUri()` read straight from this mapping.

Expand Down Expand Up @@ -118,4 +172,5 @@ Requests to the platform eVault are then made against that endpoint with the pla
1. On boot, check whether the platform eVault already exists — provision only if it doesn't.
2. Fetch an entropy token from the Registry.
3. `POST /provision` on the Provisioner with the entropy token, a fresh namespace, a verification id, and the platform public key.
4. Persist the returned `w3id` and `uri` so the platform can resolve and reuse its eVault on every subsequent boot.
4. Write a **PlatformProfile** MetaEnvelope (User-profile ontology, `acl: ["*"]`) into the eVault, including `url`, `logoUrl`, and `category` so consumers like the Marketplace can discover and render the platform.
5. Persist the returned `w3id` and `uri` (and mirror `url`/`logoUrl`/`category`) so the platform can resolve and reuse its eVault on every subsequent boot.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface PlatformProfile {
createdAt: string;
updatedAt: string;
isArchived: boolean;
url: string;
logoUrl: string;
category: string;
}

export class PlatformEVaultService {
Expand Down Expand Up @@ -132,6 +135,9 @@ export class PlatformEVaultService {
description:
"Cerberus - Secure messaging and group management platform",
version: "1.0.0",
url: "https://cerberus.w3ds.metastate.foundation",
logoUrl: "",
category: "Social",
};

await mappingRepository.save(mapping);
Expand Down Expand Up @@ -205,6 +211,9 @@ export class PlatformEVaultService {
createdAt: now,
updatedAt: now,
isArchived: false,
url: "https://cerberus.w3ds.metastate.foundation",
logoUrl: "",
category: "Social",
};

for (let attempt = 1; attempt <= maxRetries; attempt++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface PlatformProfile {
createdAt: string;
updatedAt: string;
isArchived: boolean;
url: string;
logoUrl: string;
category: string;
}

export class PlatformEVaultService {
Expand Down Expand Up @@ -132,6 +135,9 @@ export class PlatformEVaultService {
description:
"DreamSync - AI-powered wishlist matching and collaboration platform",
version: "1.0.0",
url: "https://dreamsync.w3ds.metastate.foundation",
logoUrl: "",
category: "Wellness",
};

await mappingRepository.save(mapping);
Expand Down Expand Up @@ -205,6 +211,9 @@ export class PlatformEVaultService {
createdAt: now,
updatedAt: now,
isArchived: false,
url: "https://dreamsync.w3ds.metastate.foundation",
logoUrl: "",
category: "Wellness",
};

for (let attempt = 1; attempt <= maxRetries; attempt++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface PlatformProfile {
createdAt: string;
updatedAt: string;
isArchived: boolean;
url: string;
logoUrl: string;
category: string;
}

export class PlatformEVaultService {
Expand Down Expand Up @@ -132,6 +135,9 @@ export class PlatformEVaultService {
description:
"eCurrency - Digital currency and ledger management platform",
version: "1.0.0",
url: "https://ecurrency.w3ds.metastate.foundation",
logoUrl: "",
category: "Finance",
};

await mappingRepository.save(mapping);
Expand Down Expand Up @@ -205,6 +211,9 @@ export class PlatformEVaultService {
createdAt: now,
updatedAt: now,
isArchived: false,
url: "https://ecurrency.w3ds.metastate.foundation",
logoUrl: "",
category: "Finance",
};

for (let attempt = 1; attempt <= maxRetries; attempt++) {
Expand Down
9 changes: 9 additions & 0 deletions platforms/esigner/api/src/services/PlatformEVaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface PlatformProfile {
createdAt: string;
updatedAt: string;
isArchived: boolean;
url: string;
logoUrl: string;
category: string;
}

export class PlatformEVaultService {
Expand Down Expand Up @@ -132,6 +135,9 @@ export class PlatformEVaultService {
description:
"eSigner - Digital signature and document signing platform",
version: "1.0.0",
url: "https://esigner.w3ds.metastate.foundation",
logoUrl: "",
category: "Productivity",
};

await mappingRepository.save(mapping);
Expand Down Expand Up @@ -205,6 +211,9 @@ export class PlatformEVaultService {
createdAt: now,
updatedAt: now,
isArchived: false,
url: "https://esigner.w3ds.metastate.foundation",
logoUrl: "",
category: "Productivity",
};

for (let attempt = 1; attempt <= maxRetries; attempt++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface PlatformProfile {
createdAt: string;
updatedAt: string;
isArchived: boolean;
url: string;
logoUrl: string;
category: string;
}

export class PlatformEVaultService {
Expand Down Expand Up @@ -132,6 +135,9 @@ export class PlatformEVaultService {
description:
"File Manager - Cloud storage and file management platform",
version: "1.0.0",
url: "https://file-manager.w3ds.metastate.foundation",
logoUrl: "",
category: "Storage",
};

await mappingRepository.save(mapping);
Expand Down Expand Up @@ -205,6 +211,9 @@ export class PlatformEVaultService {
createdAt: now,
updatedAt: now,
isArchived: false,
url: "https://file-manager.w3ds.metastate.foundation",
logoUrl: "",
category: "Storage",
};

for (let attempt = 1; attempt <= maxRetries; attempt++) {
Expand Down
17 changes: 16 additions & 1 deletion platforms/marketplace/client/client/src/pages/app-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ export default function AppDetailPage() {
const [, params] = useRoute("/app/:id");
const appId = params?.id;

const app = appsData.find(a => a.id === appId);
// Live platforms from awareness, used to resolve apps not in the static list.
const { data: platformsData, isLoading: platformsLoading } = useQuery<{ platforms: any[] }>({
queryKey: ["/api/platforms"],
});

const staticApp = appsData.find(a => a.id === appId);
const dynamicApp = platformsData?.platforms?.find((p: any) => p.id === appId);
const app: any = staticApp ?? dynamicApp;
const details = appId ? appDetails[appId] : null;

// Fetch platform references from eReputation
Expand All @@ -69,6 +76,14 @@ export default function AppDetailPage() {
const references = (referencesData as any)?.references || [];


if (!app && platformsLoading) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<p className="text-gray-600 font-medium">Loading…</p>
</div>
);
}

if (!app) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
Expand Down
37 changes: 33 additions & 4 deletions platforms/marketplace/client/client/src/pages/home-page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
import { useState } from "react";
import { Link } from "wouter";
import { useQuery } from "@tanstack/react-query";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Search, Store, ArrowRight } from "lucide-react";
import w3dsLogo from "@assets/w3dslogo.svg";
import appsData from "@/data/apps.json";

type App = {
id: string;
name: string;
description: string;
category: string;
logoUrl?: string | null;
url?: string;
appStoreUrl?: string;
playStoreUrl?: string;
};

const BASE_CATEGORIES = ["Identity", "Social", "Governance", "Wellness", "Finance", "Storage", "Productivity"];

export default function HomePage() {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All Apps");

const apps = appsData;
const isLoadingApps = false;
// Live platforms published to awareness; merged with the curated static list.
const { data: platformsData, isLoading: isLoadingApps } = useQuery<{ platforms: App[] }>({
queryKey: ["/api/platforms"],
});

const staticApps = appsData as App[];
const staticIds = new Set(staticApps.map((a) => a.id.toLowerCase()));
const staticNames = new Set(staticApps.map((a) => a.name.toLowerCase()));

// Static entries win on collision (they carry richer logos/URLs/descriptions).
const dynamicApps = (platformsData?.platforms ?? []).filter(
(p) => !staticIds.has(p.id.toLowerCase()) && !staticNames.has(p.name.toLowerCase()),
);
const apps: App[] = [...staticApps, ...dynamicApps];

const categories = ["All Apps", "Identity", "Social", "Governance", "Wellness", "Finance", "Storage", "Productivity"];
const categories = [
"All Apps",
...Array.from(new Set([...BASE_CATEGORIES, ...apps.map((a) => a.category)])),
];
Comment on lines +38 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add defensive checks for external API payload fields.

Data retrieved from external APIs may be missing expected fields. If an item in platformsData.platforms lacks an id, name, or description (used in the search filter logic later), calling .toLowerCase() on them will crash the React render cycle. Additionally, undefined categories will be added as "undefined" string literals to the filter list.

🛡️ Proposed defensive guards
-  const dynamicApps = (platformsData?.platforms ?? []).filter(
-    (p) => !staticIds.has(p.id.toLowerCase()) && !staticNames.has(p.name.toLowerCase()),
-  );
+  const dynamicApps = (platformsData?.platforms ?? []).filter(
+    (p) => p?.id && p?.name && p?.description && !staticIds.has(p.id.toLowerCase()) && !staticNames.has(p.name.toLowerCase()),
+  );
   const apps: App[] = [...staticApps, ...dynamicApps];

   const categories = [
     "All Apps",
-    ...Array.from(new Set([...BASE_CATEGORIES, ...apps.map((a) => a.category)])),
+    ...Array.from(new Set([...BASE_CATEGORIES, ...apps.map((a) => a.category).filter(Boolean)])),
   ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dynamicApps = (platformsData?.platforms ?? []).filter(
(p) => !staticIds.has(p.id.toLowerCase()) && !staticNames.has(p.name.toLowerCase()),
);
const apps: App[] = [...staticApps, ...dynamicApps];
const categories = ["All Apps", "Identity", "Social", "Governance", "Wellness", "Finance", "Storage", "Productivity"];
const categories = [
"All Apps",
...Array.from(new Set([...BASE_CATEGORIES, ...apps.map((a) => a.category)])),
];
const dynamicApps = (platformsData?.platforms ?? []).filter(
(p) => p?.id && p?.name && p?.description && !staticIds.has(p.id.toLowerCase()) && !staticNames.has(p.name.toLowerCase()),
);
const apps: App[] = [...staticApps, ...dynamicApps];
const categories = [
"All Apps",
...Array.from(new Set([...BASE_CATEGORIES, ...apps.map((a) => a.category).filter(Boolean)])),
];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platforms/marketplace/client/client/src/pages/home-page.tsx` around lines 38
- 46, Update the dynamic platform processing around the platforms filter and
category construction to safely handle missing id, name, description, and
category fields from the external payload. Guard lowercase operations in the
search-related logic, exclude or safely handle incomplete platform records
without crashing rendering, and only add defined category values to the
categories list so no "undefined" entry is produced.


const filteredApps = apps.filter(app => {
const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
Expand Down Expand Up @@ -95,7 +124,7 @@ export default function HomePage() {
</div>
</div>

{isLoadingApps ? (
{isLoadingApps && apps.length === 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-white rounded-3xl p-8 border border-gray-100 animate-pulse">
Expand Down
1 change: 1 addition & 0 deletions platforms/marketplace/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@jridgewell/trace-mapping": "^0.3.25",
"dotenv": "^16.4.5",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
Expand Down
Loading
Loading