@tangle-network/blueprint-ui 0.1.1 → 0.3.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/README.md +38 -4
- package/dist/BlueprintHostPanel-L1KKLNbr.d.ts +124 -0
- package/dist/chunk-37ADATBT.js +55 -0
- package/dist/chunk-37ADATBT.js.map +1 -0
- package/dist/chunk-5PCH2RJF.js +1540 -0
- package/dist/chunk-5PCH2RJF.js.map +1 -0
- package/dist/components.d.ts +179 -0
- package/dist/components.js +1130 -0
- package/dist/components.js.map +1 -0
- package/dist/index.d.ts +8604 -0
- package/dist/index.js +839 -0
- package/dist/index.js.map +1 -0
- package/dist/preset.d.ts +60 -0
- package/dist/preset.js +7 -0
- package/dist/preset.js.map +1 -0
- package/dist/styles.css +560 -0
- package/dist/wallet/index.d.ts +188 -0
- package/dist/wallet/index.js +466 -0
- package/dist/wallet/index.js.map +1 -0
- package/package.json +39 -9
- package/src/components/forms/JobExecutionDialog.tsx +10 -2
- package/src/components.ts +3 -0
- package/src/contracts/abi.ts +12 -0
- package/src/contracts/chains.ts +4 -3
- package/src/contracts/publicClient.ts +2 -1
- package/src/hooks/useJobPrice.test.ts +214 -0
- package/src/hooks/useJobPrice.ts +56 -2
- package/src/hooks/useProvisionProgress.ts +2 -1
- package/src/hooks/useQuotes.ts +112 -14
- package/src/hooks/useSessionAuth.ts +2 -1
- package/src/host/components/BlueprintHostHero.tsx +91 -0
- package/src/host/components/BlueprintHostPanel.tsx +24 -0
- package/src/host/index.ts +42 -0
- package/src/host/resolver.ts +204 -0
- package/src/host/types.ts +111 -0
- package/src/index.ts +41 -1
- package/src/stores/infra.ts +3 -2
- package/src/styles.css +128 -0
- package/src/test-setup.ts +1 -0
- package/src/utils/env.ts +22 -0
- package/src/wallet/detectParentOrigin.ts +74 -0
- package/src/wallet/index.ts +67 -0
- package/src/wallet/parentBridgeConnector.ts +156 -0
- package/src/wallet/parentBridgeProtocol.ts +109 -0
- package/src/wallet/parentBridgeProvider.test.ts +209 -0
- package/src/wallet/parentBridgeProvider.ts +411 -0
- package/tsconfig.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { getEnvVar } from '../utils/env';
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Types matching sandbox-runtime/src/provision_progress.rs
|
|
@@ -64,7 +65,7 @@ export function useProvisionProgress({
|
|
|
64
65
|
const [isPolling, setIsPolling] = useState(false);
|
|
65
66
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
66
67
|
|
|
67
|
-
const baseUrl = apiUrl ??
|
|
68
|
+
const baseUrl = apiUrl ?? getEnvVar('VITE_OPERATOR_API_URL') ?? 'http://localhost:9090';
|
|
68
69
|
|
|
69
70
|
const fetchProgress = useCallback(async () => {
|
|
70
71
|
if (callId == null) return;
|
package/src/hooks/useQuotes.ts
CHANGED
|
@@ -18,22 +18,39 @@ import { resolveOperatorRpc } from '../utils/resolveOperatorRpc';
|
|
|
18
18
|
|
|
19
19
|
// ── Types ──
|
|
20
20
|
|
|
21
|
+
type SecurityCommitment = {
|
|
22
|
+
asset: { kind: number; token: Address };
|
|
23
|
+
exposureBps: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ResourceCommitment = {
|
|
27
|
+
kind: number;
|
|
28
|
+
count: bigint;
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
export interface OperatorQuote {
|
|
22
32
|
operator: Address;
|
|
23
33
|
totalCost: bigint;
|
|
24
34
|
signature: `0x${string}`;
|
|
25
35
|
details: {
|
|
36
|
+
/**
|
|
37
|
+
* Address of the account that will submit `createServiceFromQuotes`.
|
|
38
|
+
* Required since tnt-core v0.13.0 — the contract enforces
|
|
39
|
+
* `requester == msg.sender` and rejects `address(0)` (no wildcard quotes).
|
|
40
|
+
*/
|
|
41
|
+
requester: Address;
|
|
26
42
|
blueprintId: bigint;
|
|
27
43
|
ttlBlocks: bigint;
|
|
28
44
|
totalCost: bigint;
|
|
29
45
|
timestamp: bigint;
|
|
30
46
|
expiry: bigint;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}[];
|
|
47
|
+
confidentiality: number;
|
|
48
|
+
securityCommitments: readonly SecurityCommitment[];
|
|
49
|
+
resourceCommitments: readonly ResourceCommitment[];
|
|
35
50
|
};
|
|
36
51
|
costRate: number;
|
|
52
|
+
teeAttested?: boolean;
|
|
53
|
+
teeProvider?: string;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
export interface UseQuotesResult {
|
|
@@ -49,6 +66,20 @@ export interface UseQuotesResult {
|
|
|
49
66
|
|
|
50
67
|
const POW_DIFFICULTY = 20;
|
|
51
68
|
const WEI_PER_TNT = 1_000_000_000_000_000_000; // 10^18
|
|
69
|
+
const RESOURCE_KIND_TO_ID = {
|
|
70
|
+
CPU: 0,
|
|
71
|
+
MemoryMB: 1,
|
|
72
|
+
StorageMB: 2,
|
|
73
|
+
NetworkEgressMB: 3,
|
|
74
|
+
NetworkIngressMB: 4,
|
|
75
|
+
GPU: 5,
|
|
76
|
+
} as const;
|
|
77
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Address;
|
|
78
|
+
const DEFAULT_RESOURCE_REQUIREMENTS = [
|
|
79
|
+
{ kind: 'CPU', count: 1 },
|
|
80
|
+
{ kind: 'MemoryMB', count: 1024 },
|
|
81
|
+
{ kind: 'StorageMB', count: 10240 },
|
|
82
|
+
] as const;
|
|
52
83
|
|
|
53
84
|
function sha256(data: Uint8Array): Uint8Array {
|
|
54
85
|
return viemSha256(data, 'bytes');
|
|
@@ -114,14 +145,71 @@ export function formatCost(totalCost: bigint): string {
|
|
|
114
145
|
return `${tnt.toLocaleString(undefined, { maximumFractionDigits: 2 })} TNT`;
|
|
115
146
|
}
|
|
116
147
|
|
|
148
|
+
function quoteConfidentiality(requireTee: boolean): number {
|
|
149
|
+
return requireTee ? 1 : 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resourceKindToId(kind: string): number {
|
|
153
|
+
const mapped = RESOURCE_KIND_TO_ID[kind as keyof typeof RESOURCE_KIND_TO_ID];
|
|
154
|
+
if (mapped === undefined) {
|
|
155
|
+
throw new Error(`Unsupported resource kind in quote: ${kind}`);
|
|
156
|
+
}
|
|
157
|
+
return mapped;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function mapJsonSecurityCommitment(sc: any): SecurityCommitment {
|
|
161
|
+
return {
|
|
162
|
+
asset: {
|
|
163
|
+
kind: sc.asset?.kind ?? 0,
|
|
164
|
+
token: (sc.asset?.token ?? ZERO_ADDRESS) as Address,
|
|
165
|
+
},
|
|
166
|
+
exposureBps: sc.exposure_bps ?? 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mapJsonResourceCommitment(resource: any): ResourceCommitment {
|
|
171
|
+
return {
|
|
172
|
+
kind: resourceKindToId(String(resource.kind ?? 'CPU')),
|
|
173
|
+
count: BigInt(resource.count ?? 0),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
117
177
|
// ── Hook ──
|
|
118
178
|
|
|
179
|
+
const ZERO_ADDRESS_LOWER = ZERO_ADDRESS.toLowerCase();
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetches signed `OperatorQuote` payloads from a set of operators for
|
|
183
|
+
* `createServiceFromQuotes`.
|
|
184
|
+
*
|
|
185
|
+
* @param operators Operators discovered from the on-chain registry.
|
|
186
|
+
* @param blueprintId Target blueprint id.
|
|
187
|
+
* @param ttlBlocks Requested service TTL in blocks.
|
|
188
|
+
* @param enabled Gate to disable fetching (e.g. while inputs settle).
|
|
189
|
+
* @param requester Address that will submit `createServiceFromQuotes` —
|
|
190
|
+
* must equal `msg.sender` at submission time. Required
|
|
191
|
+
* since tnt-core v0.13.0; the contract rejects
|
|
192
|
+
* `address(0)` and any mismatch.
|
|
193
|
+
* @param requireTee If true, asks operators for TEE-attested quotes.
|
|
194
|
+
*/
|
|
119
195
|
export function useQuotes(
|
|
120
196
|
operators: DiscoveredOperator[],
|
|
121
197
|
blueprintId: bigint,
|
|
122
198
|
ttlBlocks: bigint,
|
|
123
199
|
enabled: boolean,
|
|
200
|
+
requester: Address,
|
|
201
|
+
requireTee = false,
|
|
124
202
|
): UseQuotesResult {
|
|
203
|
+
// Defensive guard: only assert when the caller has actually opted in via
|
|
204
|
+
// `enabled`. This lets components compute `requester` from
|
|
205
|
+
// `useAccount().address` and pass `enabled=false` until the wallet connects.
|
|
206
|
+
if (enabled && (!requester || requester.toLowerCase() === ZERO_ADDRESS_LOWER)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
'useQuotes: `requester` is required and must be a non-zero address when `enabled=true`. ' +
|
|
209
|
+
'Pass `useAccount().address` from wagmi. tnt-core v0.13.0 contracts ' +
|
|
210
|
+
'reject quotes whose requester is address(0) or != msg.sender.',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
125
213
|
const [quotes, setQuotes] = useState<OperatorQuote[]>([]);
|
|
126
214
|
const [isLoading, setIsLoading] = useState(false);
|
|
127
215
|
const [isSolvingPow, setIsSolvingPow] = useState(false);
|
|
@@ -168,6 +256,8 @@ export function useQuotes(
|
|
|
168
256
|
ttlBlocks,
|
|
169
257
|
proofOfWork: proof,
|
|
170
258
|
challengeTimestamp: timestamp,
|
|
259
|
+
requireTee,
|
|
260
|
+
requester,
|
|
171
261
|
});
|
|
172
262
|
|
|
173
263
|
if (!response) throw new Error('No quote returned from operator');
|
|
@@ -194,7 +284,7 @@ export function useQuotes(
|
|
|
194
284
|
return () => {
|
|
195
285
|
cancelled = true;
|
|
196
286
|
};
|
|
197
|
-
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey]);
|
|
287
|
+
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey, requireTee, requester]);
|
|
198
288
|
|
|
199
289
|
const totalCost = quotes.reduce((sum, q) => sum + q.totalCost, 0n);
|
|
200
290
|
|
|
@@ -213,6 +303,8 @@ async function fetchPriceFromOperator(
|
|
|
213
303
|
ttlBlocks: bigint;
|
|
214
304
|
proofOfWork: Uint8Array;
|
|
215
305
|
challengeTimestamp: bigint;
|
|
306
|
+
requireTee: boolean;
|
|
307
|
+
requester: Address;
|
|
216
308
|
},
|
|
217
309
|
): Promise<OperatorQuote | null> {
|
|
218
310
|
// Try JSON endpoint (simpler, no protobuf dependency required)
|
|
@@ -225,11 +317,12 @@ async function fetchPriceFromOperator(
|
|
|
225
317
|
ttl_blocks: String(params.ttlBlocks),
|
|
226
318
|
proof_of_work: toHex(params.proofOfWork),
|
|
227
319
|
challenge_timestamp: String(params.challengeTimestamp),
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
320
|
+
require_tee: params.requireTee,
|
|
321
|
+
// tnt-core v0.13.0: bind the quote to the future caller. Operators
|
|
322
|
+
// sign this address into QuoteDetails; the contract enforces
|
|
323
|
+
// `requester == msg.sender`.
|
|
324
|
+
requester: params.requester,
|
|
325
|
+
resource_requirements: DEFAULT_RESOURCE_REQUIREMENTS,
|
|
233
326
|
}),
|
|
234
327
|
signal: AbortSignal.timeout(10_000),
|
|
235
328
|
});
|
|
@@ -242,16 +335,21 @@ async function fetchPriceFromOperator(
|
|
|
242
335
|
totalCost: BigInt(data.total_cost ?? '0'),
|
|
243
336
|
signature: (data.signature ?? '0x') as `0x${string}`,
|
|
244
337
|
costRate: Number(data.cost_rate ?? 0),
|
|
338
|
+
teeAttested: Boolean(data.tee_attested),
|
|
339
|
+
teeProvider: data.tee_provider || undefined,
|
|
245
340
|
details: {
|
|
341
|
+
// Prefer the operator-signed value; fall back to the hook's input.
|
|
342
|
+
// If the operator returns a mismatched requester the contract will
|
|
343
|
+
// revert at submission, so callers should still verify equality.
|
|
344
|
+
requester: ((data.details?.requester as Address | undefined) ?? params.requester),
|
|
246
345
|
blueprintId: BigInt(data.details?.blueprint_id ?? params.blueprintId),
|
|
247
346
|
ttlBlocks: BigInt(data.details?.ttl_blocks ?? params.ttlBlocks),
|
|
248
347
|
totalCost: BigInt(data.details?.total_cost ?? '0'),
|
|
249
348
|
timestamp: BigInt(data.details?.timestamp ?? params.challengeTimestamp),
|
|
250
349
|
expiry: BigInt(data.details?.expiry ?? '0'),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})),
|
|
350
|
+
confidentiality: Number(data.details?.confidentiality ?? quoteConfidentiality(params.requireTee)),
|
|
351
|
+
securityCommitments: (data.details?.security_commitments ?? []).map(mapJsonSecurityCommitment),
|
|
352
|
+
resourceCommitments: (data.details?.resources ?? []).map(mapJsonResourceCommitment),
|
|
255
353
|
},
|
|
256
354
|
};
|
|
257
355
|
} catch {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
import { useSignMessage } from 'wagmi';
|
|
3
3
|
import { getSession, setSession, removeSession, type SessionEntry } from '../stores/session';
|
|
4
|
+
import { getEnvVar } from '../utils/env';
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Types
|
|
@@ -28,7 +29,7 @@ interface UseSessionAuthOptions {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function useSessionAuth({ sandboxId, apiUrl }: UseSessionAuthOptions) {
|
|
31
|
-
const baseUrl = apiUrl ??
|
|
32
|
+
const baseUrl = apiUrl ?? getEnvVar('VITE_OPERATOR_API_URL') ?? 'http://localhost:9090';
|
|
32
33
|
const { signMessageAsync } = useSignMessage();
|
|
33
34
|
|
|
34
35
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../../components/ui/card';
|
|
3
|
+
import { Badge } from '../../components/ui/badge';
|
|
4
|
+
import { Button } from '../../components/ui/button';
|
|
5
|
+
import { cn } from '../../utils';
|
|
6
|
+
|
|
7
|
+
type Action = {
|
|
8
|
+
label: string;
|
|
9
|
+
href?: string;
|
|
10
|
+
onClick?: () => void;
|
|
11
|
+
variant?: React.ComponentProps<typeof Button>['variant'];
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type BlueprintHostHeroProps = {
|
|
16
|
+
title: string;
|
|
17
|
+
tagline?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
badges?: string[];
|
|
20
|
+
actions?: Action[];
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function BlueprintHostHero({
|
|
26
|
+
title,
|
|
27
|
+
tagline,
|
|
28
|
+
description,
|
|
29
|
+
badges = [],
|
|
30
|
+
actions = [],
|
|
31
|
+
children,
|
|
32
|
+
className,
|
|
33
|
+
}: BlueprintHostHeroProps) {
|
|
34
|
+
return (
|
|
35
|
+
<Card className={cn('rounded-3xl border-bp-elements-borderColor/70 bg-bp-elements-background-depth-2', className)}>
|
|
36
|
+
<CardHeader className="space-y-4">
|
|
37
|
+
{badges.length > 0 ? (
|
|
38
|
+
<div className="flex flex-wrap gap-2">
|
|
39
|
+
{badges.map((badge) => (
|
|
40
|
+
<Badge key={badge} variant="secondary">
|
|
41
|
+
{badge}
|
|
42
|
+
</Badge>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
) : null}
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
<CardTitle className="text-3xl">{title}</CardTitle>
|
|
48
|
+
{tagline ? (
|
|
49
|
+
<CardDescription className="text-base text-bp-elements-textPrimary/80">
|
|
50
|
+
{tagline}
|
|
51
|
+
</CardDescription>
|
|
52
|
+
) : null}
|
|
53
|
+
</div>
|
|
54
|
+
{description ? (
|
|
55
|
+
<p className="max-w-3xl text-sm leading-6 text-bp-elements-textSecondary">
|
|
56
|
+
{description}
|
|
57
|
+
</p>
|
|
58
|
+
) : null}
|
|
59
|
+
</CardHeader>
|
|
60
|
+
{(actions.length > 0 || children) && (
|
|
61
|
+
<CardContent className="space-y-4">
|
|
62
|
+
{actions.length > 0 ? (
|
|
63
|
+
<div className="flex flex-wrap gap-3">
|
|
64
|
+
{actions.map((action) => {
|
|
65
|
+
const button = (
|
|
66
|
+
<Button
|
|
67
|
+
key={action.label}
|
|
68
|
+
variant={action.variant ?? 'default'}
|
|
69
|
+
onClick={action.onClick}
|
|
70
|
+
disabled={action.disabled}
|
|
71
|
+
>
|
|
72
|
+
{action.label}
|
|
73
|
+
</Button>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return action.href ? (
|
|
77
|
+
<a key={action.label} href={action.href}>
|
|
78
|
+
{button}
|
|
79
|
+
</a>
|
|
80
|
+
) : (
|
|
81
|
+
button
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
{children}
|
|
87
|
+
</CardContent>
|
|
88
|
+
)}
|
|
89
|
+
</Card>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
|
+
|
|
5
|
+
export type BlueprintHostPanelProps = {
|
|
6
|
+
title: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function BlueprintHostPanel({
|
|
12
|
+
title,
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
}: BlueprintHostPanelProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Card className={cn('rounded-3xl border-bp-elements-borderColor/70 bg-bp-elements-background-depth-2', className)}>
|
|
18
|
+
<CardHeader>
|
|
19
|
+
<CardTitle className="text-xl">{title}</CardTitle>
|
|
20
|
+
</CardHeader>
|
|
21
|
+
<CardContent>{children}</CardContent>
|
|
22
|
+
</Card>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
BlueprintAppVisibility,
|
|
3
|
+
BlueprintPublisherVerification,
|
|
4
|
+
BlueprintExperienceTier,
|
|
5
|
+
BlueprintSlugPolicy,
|
|
6
|
+
BlueprintUiSurface,
|
|
7
|
+
BlueprintResourceRoute,
|
|
8
|
+
BlueprintPermissionScope,
|
|
9
|
+
BlueprintExternalAppMode,
|
|
10
|
+
BlueprintExternalAppTrust,
|
|
11
|
+
BlueprintPublisher,
|
|
12
|
+
BlueprintResourceModel,
|
|
13
|
+
BlueprintPermissionDescriptor,
|
|
14
|
+
BlueprintExternalAppConfig,
|
|
15
|
+
BlueprintUiManifest,
|
|
16
|
+
BlueprintAppModuleBinding,
|
|
17
|
+
BlueprintAppEntry,
|
|
18
|
+
BlueprintAppResolvedView,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
buildCanonicalBlueprintSlug,
|
|
23
|
+
resolveBlueprintAppView,
|
|
24
|
+
toBlueprintAppEntry,
|
|
25
|
+
getBlueprintExperienceTierLabel,
|
|
26
|
+
getBlueprintSlugPolicyLabel,
|
|
27
|
+
getBlueprintSurfaceLabel,
|
|
28
|
+
getBlueprintPublisherVerificationLabel,
|
|
29
|
+
getExternalAppTrustLabel,
|
|
30
|
+
isVerifiedBlueprintPublisher,
|
|
31
|
+
canPublisherClaimSlug,
|
|
32
|
+
isTrustedExternalAppHost,
|
|
33
|
+
getBlueprintPath,
|
|
34
|
+
getBlueprintServicePath,
|
|
35
|
+
sanitizeBlueprintSlugPart,
|
|
36
|
+
deriveBlueprintRequestedSlug,
|
|
37
|
+
} from './resolver';
|
|
38
|
+
|
|
39
|
+
export type { BlueprintHostHeroProps } from './components/BlueprintHostHero';
|
|
40
|
+
export { BlueprintHostHero } from './components/BlueprintHostHero';
|
|
41
|
+
export type { BlueprintHostPanelProps } from './components/BlueprintHostPanel';
|
|
42
|
+
export { BlueprintHostPanel } from './components/BlueprintHostPanel';
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlueprintAppEntry,
|
|
3
|
+
BlueprintAppResolvedView,
|
|
4
|
+
BlueprintExperienceTier,
|
|
5
|
+
BlueprintExternalAppTrust,
|
|
6
|
+
BlueprintPublisher,
|
|
7
|
+
BlueprintPublisherVerification,
|
|
8
|
+
BlueprintSlugPolicy,
|
|
9
|
+
BlueprintUiSurface,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
const EXPERIENCE_TIER_LABELS: Record<BlueprintExperienceTier, string> = {
|
|
13
|
+
generic: 'Protocol fallback',
|
|
14
|
+
declarative: 'Declarative blueprint UI',
|
|
15
|
+
'curated-module': 'Curated app module',
|
|
16
|
+
'external-app': 'External app handoff',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const SLUG_POLICY_LABELS: Record<BlueprintSlugPolicy, string> = {
|
|
20
|
+
reserved: 'Reserved slug',
|
|
21
|
+
'publisher-scoped': 'Publisher-scoped slug',
|
|
22
|
+
'public-requested': 'Public requested slug',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SURFACE_LABELS: Record<BlueprintUiSurface, string> = {
|
|
26
|
+
'generic-overview': 'Generic overview',
|
|
27
|
+
'service-explorer': 'Service explorer',
|
|
28
|
+
'service-console': 'Service console',
|
|
29
|
+
'actions-panel': 'Actions panel',
|
|
30
|
+
resources: 'Resources',
|
|
31
|
+
chat: 'Chat',
|
|
32
|
+
vaults: 'Vaults',
|
|
33
|
+
metrics: 'Metrics',
|
|
34
|
+
permissions: 'Permissions',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const PUBLISHER_VERIFICATION_LABELS: Record<
|
|
38
|
+
BlueprintPublisherVerification,
|
|
39
|
+
string
|
|
40
|
+
> = {
|
|
41
|
+
'first-party': 'First-party publisher',
|
|
42
|
+
verified: 'Verified publisher',
|
|
43
|
+
unverified: 'Unverified publisher',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const EXTERNAL_APP_TRUST_LABELS: Record<BlueprintExternalAppTrust, string> = {
|
|
47
|
+
trusted: 'Trusted external app',
|
|
48
|
+
restricted: 'Restricted external app',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function buildCanonicalBlueprintSlug(entry: BlueprintAppEntry): string {
|
|
52
|
+
if (entry.canonicalSlug) {
|
|
53
|
+
return entry.canonicalSlug;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (entry.slugPolicy === 'reserved' || !entry.publisher.namespace) {
|
|
57
|
+
return entry.slug;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return `@${entry.publisher.namespace}/${entry.slug}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveBlueprintAppView(
|
|
64
|
+
entry: BlueprintAppEntry,
|
|
65
|
+
): BlueprintAppResolvedView {
|
|
66
|
+
return {
|
|
67
|
+
slug: entry.slug,
|
|
68
|
+
canonicalSlug: buildCanonicalBlueprintSlug(entry),
|
|
69
|
+
blueprintId: entry.blueprintId,
|
|
70
|
+
publisher: entry.publisher,
|
|
71
|
+
tier: entry.tier,
|
|
72
|
+
slugPolicy: entry.slugPolicy,
|
|
73
|
+
manifest: entry.manifest,
|
|
74
|
+
module: entry.module,
|
|
75
|
+
fallbackEnabled: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function toBlueprintAppEntry(
|
|
80
|
+
view: BlueprintAppResolvedView,
|
|
81
|
+
): BlueprintAppEntry {
|
|
82
|
+
return {
|
|
83
|
+
slug: view.slug,
|
|
84
|
+
canonicalSlug: view.canonicalSlug,
|
|
85
|
+
blueprintId: view.blueprintId,
|
|
86
|
+
publisher: view.publisher,
|
|
87
|
+
tier: view.tier,
|
|
88
|
+
slugPolicy: view.slugPolicy,
|
|
89
|
+
manifest: view.manifest,
|
|
90
|
+
module: view.module,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getBlueprintExperienceTierLabel(
|
|
95
|
+
tier: BlueprintExperienceTier,
|
|
96
|
+
): string {
|
|
97
|
+
return EXPERIENCE_TIER_LABELS[tier];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getBlueprintSlugPolicyLabel(policy: BlueprintSlugPolicy): string {
|
|
101
|
+
return SLUG_POLICY_LABELS[policy];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getBlueprintSurfaceLabel(surface: BlueprintUiSurface): string {
|
|
105
|
+
return SURFACE_LABELS[surface];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getBlueprintPublisherVerificationLabel(
|
|
109
|
+
verification: BlueprintPublisherVerification,
|
|
110
|
+
): string {
|
|
111
|
+
return PUBLISHER_VERIFICATION_LABELS[verification];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getExternalAppTrustLabel(
|
|
115
|
+
trust: BlueprintExternalAppTrust,
|
|
116
|
+
): string {
|
|
117
|
+
return EXTERNAL_APP_TRUST_LABELS[trust];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isVerifiedBlueprintPublisher(
|
|
121
|
+
publisher: BlueprintPublisher,
|
|
122
|
+
): boolean {
|
|
123
|
+
return (
|
|
124
|
+
publisher.verification === 'first-party' ||
|
|
125
|
+
publisher.verification === 'verified'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function canPublisherClaimSlug(
|
|
130
|
+
slug: string,
|
|
131
|
+
publisher?: Pick<BlueprintPublisher, 'namespace' | 'verification'>,
|
|
132
|
+
reservedSlugs: Set<string> = new Set(),
|
|
133
|
+
): boolean {
|
|
134
|
+
if (reservedSlugs.has(slug)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
publisher?.namespace !== undefined &&
|
|
140
|
+
publisher.namespace.trim().length > 0 &&
|
|
141
|
+
(publisher.verification === 'verified' ||
|
|
142
|
+
publisher.verification === 'first-party')
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isTrustedExternalAppHost(
|
|
147
|
+
host: string,
|
|
148
|
+
trustedHosts: readonly string[] = [],
|
|
149
|
+
): boolean {
|
|
150
|
+
const normalizedHost = host.trim().toLowerCase();
|
|
151
|
+
|
|
152
|
+
return trustedHosts.some(
|
|
153
|
+
(trustedHost) =>
|
|
154
|
+
normalizedHost === trustedHost || normalizedHost.endsWith(`.${trustedHost}`),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getBlueprintPath(view: BlueprintAppResolvedView): string {
|
|
159
|
+
if (view.tier === 'curated-module' || view.tier === 'external-app') {
|
|
160
|
+
return `/blueprints/${view.canonicalSlug}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (view.blueprintId !== undefined) {
|
|
164
|
+
return `/blueprints/${view.blueprintId.toString()}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return `/blueprints/${view.slug}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getBlueprintServicePath(
|
|
171
|
+
view: BlueprintAppResolvedView,
|
|
172
|
+
serviceId: string,
|
|
173
|
+
): string {
|
|
174
|
+
if (view.tier === 'curated-module' || view.tier === 'external-app') {
|
|
175
|
+
return `${getBlueprintPath(view)}/${serviceId}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return `${getBlueprintPath(view)}/services/${serviceId}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function sanitizeBlueprintSlugPart(value: string): string {
|
|
182
|
+
return value
|
|
183
|
+
.trim()
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
186
|
+
.replace(/^-+|-+$/g, '')
|
|
187
|
+
.replace(/-{2,}/g, '-');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function deriveBlueprintRequestedSlug(
|
|
191
|
+
blueprint: Pick<{ name: string; author: string; id: bigint }, 'name' | 'author' | 'id'>,
|
|
192
|
+
): string {
|
|
193
|
+
const fromName = sanitizeBlueprintSlugPart(blueprint.name);
|
|
194
|
+
if (fromName.length > 0) {
|
|
195
|
+
return fromName;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const fromAuthor = sanitizeBlueprintSlugPart(blueprint.author);
|
|
199
|
+
if (fromAuthor.length > 0) {
|
|
200
|
+
return `${fromAuthor}-${blueprint.id.toString()}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `blueprint-${blueprint.id.toString()}`;
|
|
204
|
+
}
|