frontier-os-app-builder 1.0.0 → 1.2.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/LICENSE +21 -0
- package/README.md +90 -14
- package/agents/fos-executor.md +105 -39
- package/agents/fos-plan-checker.md +62 -25
- package/agents/fos-planner.md +80 -72
- package/agents/fos-researcher.md +26 -15
- package/agents/fos-verifier.md +96 -27
- package/bin/fos-tools.cjs +146 -42
- package/bin/install.js +8 -5
- package/commands/fos/add-feature.md +1 -2
- package/commands/fos/discuss.md +0 -1
- package/commands/fos/new-app.md +2 -4
- package/commands/fos/new-milestone.md +1 -1
- package/commands/fos/plan.md +0 -2
- package/package.json +7 -1
- package/references/app-patterns.md +128 -21
- package/references/deployment.md +40 -124
- package/references/module-index.md +32 -0
- package/references/sdk/chain.md +92 -0
- package/references/sdk/communities.md +159 -0
- package/references/sdk/events.md +212 -0
- package/references/sdk/init.md +126 -0
- package/references/sdk/navigation.md +49 -0
- package/references/sdk/offices.md +76 -0
- package/references/sdk/partnerships.md +111 -0
- package/references/sdk/storage.md +44 -0
- package/references/sdk/thirdparty.md +240 -0
- package/references/sdk/token-amount.md +99 -0
- package/references/sdk/types.md +27 -0
- package/references/sdk/ui-utils.md +39 -0
- package/references/sdk/user.md +208 -0
- package/references/sdk/wallet.md +334 -0
- package/references/verification-rules.md +111 -50
- package/templates/app/frontier-services.tsx +871 -0
- package/templates/app/layout-standalone.tsx +8 -0
- package/templates/app/layout.tsx +19 -9
- package/templates/app/package-standalone.json +35 -0
- package/templates/app/package.json +2 -1
- package/templates/app/public/favicon.svg +3 -0
- package/templates/app/sdk-context.tsx +7 -9
- package/templates/app/sdk-services.tsx +98 -0
- package/templates/app/vercel-standalone.json +5 -0
- package/templates/app/vercel.json +8 -95
- package/templates/state/plan.md +32 -14
- package/templates/state/requirements.md +1 -1
- package/templates/state/roadmap.md +57 -24
- package/templates/state/summary.md +27 -30
- package/workflows/add-feature.md +6 -1
- package/workflows/discuss.md +126 -11
- package/workflows/execute-plan.md +21 -14
- package/workflows/execute.md +204 -24
- package/workflows/new-app.md +64 -23
- package/workflows/new-milestone.md +10 -3
- package/workflows/plan.md +16 -5
- package/workflows/ship.md +91 -34
- package/workflows/status.md +1 -2
- package/references/module-inference.md +0 -349
- package/references/sdk-surface.md +0 -1622
- package/templates/app/main-simple.tsx +0 -19
- package/templates/state/manifest.json +0 -11
|
@@ -13,7 +13,7 @@ Reference for the standard structure, conventions, and tech stack used by all Fr
|
|
|
13
13
|
| Language | TypeScript | 5.9 |
|
|
14
14
|
| CSS | Tailwind CSS (via PostCSS) | 4 |
|
|
15
15
|
| Testing | Vitest + jsdom + Testing Library| 4 / 27 |
|
|
16
|
-
| SDK | @frontiertower/frontier-sdk | 0.
|
|
16
|
+
| SDK | @frontiertower/frontier-sdk | 0.24.0 |
|
|
17
17
|
| Routing | react-router-dom | 7 |
|
|
18
18
|
|
|
19
19
|
---
|
|
@@ -34,7 +34,9 @@ app-<name>/
|
|
|
34
34
|
main.tsx # React root + RouterProvider (identical pattern)
|
|
35
35
|
router.tsx # Route definitions (parameterized)
|
|
36
36
|
lib/
|
|
37
|
-
|
|
37
|
+
frontier-services.tsx # useServices() provider + mock services (identical across all apps)
|
|
38
|
+
sdk-context.tsx # SdkProvider + useSdk hook (added during SDK Integration phase)
|
|
39
|
+
sdk-services.tsx # SDK adapter mapping services to real SDK (added during SDK Integration phase)
|
|
38
40
|
views/
|
|
39
41
|
Layout.tsx # Shell layout component (parameterized)
|
|
40
42
|
<FeatureViews>.tsx # App-specific view components
|
|
@@ -65,10 +67,14 @@ app-<name>/
|
|
|
65
67
|
|
|
66
68
|
These files are copied verbatim. They must not be modified per app.
|
|
67
69
|
|
|
68
|
-
### `src/lib/
|
|
70
|
+
### `src/lib/frontier-services.tsx`
|
|
71
|
+
|
|
72
|
+
This file provides the `useServices()` hook and `FrontierServicesProvider`. It is identical across all apps and stays **SDK-free** (imports only React) — it is the mock seam. During standalone development `FrontierServicesProvider` returns mock services; after SDK Integration the **Layout** passes it real SDK-backed services in-frame (mocks standalone), so feature code never changes.
|
|
73
|
+
|
|
74
|
+
### `src/lib/sdk-context.tsx` — identical across all apps, created during SDK Integration phase (not at scaffold time)
|
|
69
75
|
|
|
70
76
|
```tsx
|
|
71
|
-
import { createContext, useContext, useEffect,
|
|
77
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
72
78
|
import { FrontierSDK } from '@frontiertower/frontier-sdk';
|
|
73
79
|
|
|
74
80
|
const SdkContext = createContext<FrontierSDK | null>(null);
|
|
@@ -80,29 +86,33 @@ export const useSdk = (): FrontierSDK => {
|
|
|
80
86
|
};
|
|
81
87
|
|
|
82
88
|
export const SdkProvider = ({ children }: { children: ReactNode }) => {
|
|
83
|
-
const
|
|
84
|
-
const [ready, setReady] = useState(false);
|
|
89
|
+
const [sdk, setSdk] = useState<FrontierSDK | null>(null);
|
|
85
90
|
|
|
86
91
|
useEffect(() => {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
setReady(true);
|
|
92
|
+
const instance = new FrontierSDK();
|
|
93
|
+
setSdk(instance);
|
|
90
94
|
|
|
91
95
|
return () => {
|
|
92
|
-
|
|
96
|
+
instance.destroy();
|
|
93
97
|
};
|
|
94
98
|
}, []);
|
|
95
99
|
|
|
96
|
-
if (!
|
|
100
|
+
if (!sdk) return null;
|
|
97
101
|
|
|
98
102
|
return (
|
|
99
|
-
<SdkContext.Provider value={
|
|
103
|
+
<SdkContext.Provider value={sdk}>
|
|
100
104
|
{children}
|
|
101
105
|
</SdkContext.Provider>
|
|
102
106
|
);
|
|
103
107
|
};
|
|
104
108
|
```
|
|
105
109
|
|
|
110
|
+
> The SDK is held in `useState` (not a `useRef`): under React StrictMode the effect runs mount → cleanup → mount, so the first instance is created then destroyed. Storing it in state makes the second `setSdk(...)` re-render the Provider with the live instance, instead of leaving consumers pinned to the destroyed one. Do not "simplify" this back to a ref.
|
|
111
|
+
|
|
112
|
+
### `src/lib/sdk-services.tsx` — identical across all apps, created during SDK Integration phase (not at scaffold time)
|
|
113
|
+
|
|
114
|
+
This file provides the adapter that maps the `FrontierServices` interface to real SDK calls. It is created during the SDK Integration phase.
|
|
115
|
+
|
|
106
116
|
### `postcss.config.js`
|
|
107
117
|
|
|
108
118
|
```js
|
|
@@ -142,7 +152,7 @@ export default {
|
|
|
142
152
|
|
|
143
153
|
### `vercel.json`
|
|
144
154
|
|
|
145
|
-
See [deployment.md](deployment.md) for the full file. All apps share the same CORS
|
|
155
|
+
See [deployment.md](deployment.md) for the full file. All apps share the same `vercel.json`: CORS for the production origin plus a `Content-Security-Policy: frame-ancestors` listing the 3 live Frontier OS origins (production `os.frontiertower.io`, sandbox `sandbox.os.frontiertower.io`, and `localhost:5173`) and the standard security headers.
|
|
146
156
|
|
|
147
157
|
---
|
|
148
158
|
|
|
@@ -165,7 +175,7 @@ Fixed fields (do not change):
|
|
|
165
175
|
- `"private": true`
|
|
166
176
|
- `"type": "module"`
|
|
167
177
|
- `scripts` block (see Package Scripts below)
|
|
168
|
-
- Core dependencies: `@frontiertower/frontier-sdk`, `react`, `react-dom`, `react-router-dom`
|
|
178
|
+
- Core dependencies: `@frontiertower/frontier-sdk`, `react`, `react-dom`, `react-router-dom`, `viem` (`^2.44.0` — for on-chain apps that build calldata for `executeCall`/`executeBatchCall`; safe to drop for pure-UI apps)
|
|
169
179
|
- Core devDependencies: `@tailwindcss/postcss`, `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `postcss`, `tailwindcss`, `typescript`, `vite`
|
|
170
180
|
- Test devDependencies (when tests exist): `@testing-library/jest-dom`, `@testing-library/react`, `@testing-library/user-event`, `@vitest/coverage-v8`, `jsdom`, `vitest`
|
|
171
181
|
|
|
@@ -308,9 +318,14 @@ export const Layout = () => {
|
|
|
308
318
|
);
|
|
309
319
|
}
|
|
310
320
|
|
|
321
|
+
// In-frame: SdkProvider provides the SDK; SdkServicesBridge wires it into
|
|
322
|
+
// FrontierServicesProvider so feature code's useServices() works unchanged.
|
|
323
|
+
// (SdkServicesBridge helper — see templates/app/layout.tsx.)
|
|
311
324
|
return (
|
|
312
325
|
<SdkProvider>
|
|
313
|
-
<
|
|
326
|
+
<SdkServicesBridge>
|
|
327
|
+
<Outlet />
|
|
328
|
+
</SdkServicesBridge>
|
|
314
329
|
</SdkProvider>
|
|
315
330
|
);
|
|
316
331
|
};
|
|
@@ -320,6 +335,69 @@ The `isInFrontierApp()` function checks `window.self !== window.top` -- it retur
|
|
|
320
335
|
|
|
321
336
|
The `createStandaloneHTML()` fallback renders a branded page telling the user to open the app inside Frontier Wallet.
|
|
322
337
|
|
|
338
|
+
#### Standalone-First Layout (Phase 1 through feature phases)
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { Outlet } from 'react-router-dom';
|
|
342
|
+
import { FrontierServicesProvider } from '../lib/frontier-services';
|
|
343
|
+
|
|
344
|
+
export const Layout = () => (
|
|
345
|
+
<FrontierServicesProvider>
|
|
346
|
+
<Outlet />
|
|
347
|
+
</FrontierServicesProvider>
|
|
348
|
+
);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
No iframe detection, no loading state, no standalone fallback. The app just renders with mock services.
|
|
352
|
+
|
|
353
|
+
#### SDK-Aware Layout (after SDK Integration phase)
|
|
354
|
+
|
|
355
|
+
The Layout pattern with `isInFrontierApp()`, `createStandaloneHTML()`, `SdkProvider`, AND the `FrontierServicesProvider` bridge (so `useServices()` works against the real SDK) is applied during the SDK Integration phase. See the SDK-Aware Layout Pattern above and `templates/app/layout.tsx`.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### Services Pattern (Standalone-First)
|
|
360
|
+
|
|
361
|
+
New apps use the `useServices()` abstraction instead of `useSdk()` directly. This enables standalone development with mock data before the SDK is wired in.
|
|
362
|
+
|
|
363
|
+
**Standard hook using services:**
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { useState, useEffect } from 'react';
|
|
367
|
+
import { formatAmount } from '@frontiertower/frontier-sdk';
|
|
368
|
+
import { useServices } from '../lib/frontier-services';
|
|
369
|
+
|
|
370
|
+
export function useBalance() {
|
|
371
|
+
const services = useServices();
|
|
372
|
+
// Balance fields are bigint base units; format them for display with formatAmount().
|
|
373
|
+
const [balance, setBalance] = useState<string | null>(null);
|
|
374
|
+
const [loading, setLoading] = useState(true);
|
|
375
|
+
const [error, setError] = useState<string | null>(null);
|
|
376
|
+
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
const fetch = async () => {
|
|
379
|
+
try {
|
|
380
|
+
const result = await services.wallet.getBalance();
|
|
381
|
+
setBalance(formatAmount(result.total));
|
|
382
|
+
} catch (err) {
|
|
383
|
+
setError(err instanceof Error ? err.message : 'Failed to load balance');
|
|
384
|
+
} finally {
|
|
385
|
+
setLoading(false);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
fetch();
|
|
389
|
+
}, [services]);
|
|
390
|
+
|
|
391
|
+
return { balance, loading, error };
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Key differences from SDK pattern:**
|
|
396
|
+
- Import `useServices` from `../lib/frontier-services` (not `useSdk` from `../lib/sdk-context`)
|
|
397
|
+
- Access modules as properties: `services.wallet` (not `sdk.getWallet()`)
|
|
398
|
+
- Types imported from `../lib/frontier-services` (not `@frontiertower/frontier-sdk`)
|
|
399
|
+
- Works standalone with mock data — no iframe required during development
|
|
400
|
+
|
|
323
401
|
---
|
|
324
402
|
|
|
325
403
|
## Router Variant (Standard)
|
|
@@ -345,13 +423,25 @@ export const router = createBrowserRouter([
|
|
|
345
423
|
]);
|
|
346
424
|
```
|
|
347
425
|
|
|
348
|
-
### Single
|
|
426
|
+
### Simple Apps (Single View)
|
|
427
|
+
|
|
428
|
+
Even an app with only one view uses the router — there is no "render the Layout directly from `main.tsx`" path. Define a single index route so `Layout` stays in the render tree:
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
import { createBrowserRouter } from 'react-router-dom';
|
|
432
|
+
import { Layout } from './views/Layout';
|
|
433
|
+
import { Home } from './views/Home';
|
|
434
|
+
|
|
435
|
+
export const router = createBrowserRouter([
|
|
436
|
+
{
|
|
437
|
+
path: '/',
|
|
438
|
+
element: <Layout />,
|
|
439
|
+
children: [{ index: true, element: <Home /> }],
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
```
|
|
349
443
|
|
|
350
|
-
|
|
351
|
-
- `main.tsx` renders the Layout directly instead of `<RouterProvider>`
|
|
352
|
-
- `Layout.tsx` renders the single view component instead of `<Outlet />`
|
|
353
|
-
- No `router.tsx` file needed
|
|
354
|
-
- `react-router-dom` can be removed from dependencies
|
|
444
|
+
Keeping `Layout` mounted preserves the services seam: standalone it provides `FrontierServicesProvider`, and after SDK Integration it wraps the app in `SdkProvider` + `SdkServicesBridge` so `useServices()` resolves against the real SDK. `react-router-dom` stays a dependency. Do not bypass the router by rendering a view straight from `main.tsx` — that skips the Layout bridge and ships mock services in-frame.
|
|
355
445
|
|
|
356
446
|
### `main.tsx` (Identical Pattern)
|
|
357
447
|
|
|
@@ -499,3 +589,20 @@ Configured in `vite.config.ts`:
|
|
|
499
589
|
|
|
500
590
|
- `"types": ["vitest/globals", "@testing-library/jest-dom"]` -- global type augmentation
|
|
501
591
|
- `"exclude": ["src/test", "**/*.test.ts", "**/*.test.tsx"]` -- test files excluded from production type-check (`tsc --noEmit` / `tsc && vite build`)
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
### SDK Integration Pattern
|
|
596
|
+
|
|
597
|
+
The final phase of every app wires the real Frontier SDK in. This is a mechanical step:
|
|
598
|
+
|
|
599
|
+
1. **Add SDK dependency**: `npm install @frontiertower/frontier-sdk`
|
|
600
|
+
2. **Create `src/lib/sdk-context.tsx`**: Standard SdkProvider + useSdk hook (from template)
|
|
601
|
+
3. **Create `src/lib/sdk-services.tsx`**: Adapter mapping FrontierServices interface to real SDK calls
|
|
602
|
+
4. **Leave `src/lib/frontier-services.tsx` unchanged**: it stays the SDK-free mock seam. The iframe/standalone switch happens in Layout (step 5) — do NOT add SDK imports or detection here.
|
|
603
|
+
5. **Swap in `src/views/Layout.tsx`** (from `templates/app/layout.tsx`): `isInFrontierApp()` detection + standalone fallback; in-frame it wraps the app in `SdkProvider` AND bridges the SDK into `FrontierServicesProvider` (via `createSdkServices(sdk)`) so `useServices()` resolves against the real SDK
|
|
604
|
+
6. **Swap in the full `vercel.json`**: CORS for the production origin + `Content-Security-Policy: frame-ancestors` listing the 3 live origins (`os.frontiertower.io`, `sandbox.os.frontiertower.io`, `localhost:5173`) + security headers
|
|
605
|
+
|
|
606
|
+
After SDK Integration, the app works in both modes:
|
|
607
|
+
- **Standalone** (browser): Uses mock services, shows development data
|
|
608
|
+
- **Iframe** (Frontier PWA): Uses real SDK, shows live data
|
package/references/deployment.md
CHANGED
|
@@ -18,13 +18,6 @@ All Frontier OS apps deploy to Vercel. The `vercel.json` file is identical acros
|
|
|
18
18
|
"headers": [
|
|
19
19
|
{
|
|
20
20
|
"source": "/(.*)",
|
|
21
|
-
"has": [
|
|
22
|
-
{
|
|
23
|
-
"type": "header",
|
|
24
|
-
"key": "Origin",
|
|
25
|
-
"value": "https://os.frontiertower.io"
|
|
26
|
-
}
|
|
27
|
-
],
|
|
28
21
|
"headers": [
|
|
29
22
|
{
|
|
30
23
|
"key": "Access-Control-Allow-Origin",
|
|
@@ -37,102 +30,22 @@ All Frontier OS apps deploy to Vercel. The `vercel.json` file is identical acros
|
|
|
37
30
|
{
|
|
38
31
|
"key": "Access-Control-Allow-Headers",
|
|
39
32
|
"value": "Content-Type"
|
|
40
|
-
}
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"source": "/(.*)",
|
|
45
|
-
"has": [
|
|
46
|
-
{
|
|
47
|
-
"type": "header",
|
|
48
|
-
"key": "Origin",
|
|
49
|
-
"value": "https://alpha.os.frontiertower.io"
|
|
50
|
-
}
|
|
51
|
-
],
|
|
52
|
-
"headers": [
|
|
53
|
-
{
|
|
54
|
-
"key": "Access-Control-Allow-Origin",
|
|
55
|
-
"value": "https://alpha.os.frontiertower.io"
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"key": "Access-Control-Allow-Methods",
|
|
59
|
-
"value": "GET, OPTIONS"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"key": "Access-Control-Allow-Headers",
|
|
63
|
-
"value": "Content-Type"
|
|
64
|
-
}
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"source": "/(.*)",
|
|
69
|
-
"has": [
|
|
70
|
-
{
|
|
71
|
-
"type": "header",
|
|
72
|
-
"key": "Origin",
|
|
73
|
-
"value": "https://beta.os.frontiertower.io"
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"headers": [
|
|
77
|
-
{
|
|
78
|
-
"key": "Access-Control-Allow-Origin",
|
|
79
|
-
"value": "https://beta.os.frontiertower.io"
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"key": "Access-Control-Allow-Methods",
|
|
83
|
-
"value": "GET, OPTIONS"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"key": "Access-Control-Allow-Headers",
|
|
87
|
-
"value": "Content-Type"
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"source": "/(.*)",
|
|
93
|
-
"has": [
|
|
94
|
-
{
|
|
95
|
-
"type": "header",
|
|
96
|
-
"key": "Origin",
|
|
97
|
-
"value": "https://sandbox.os.frontiertower.io"
|
|
98
|
-
}
|
|
99
|
-
],
|
|
100
|
-
"headers": [
|
|
101
|
-
{
|
|
102
|
-
"key": "Access-Control-Allow-Origin",
|
|
103
|
-
"value": "https://sandbox.os.frontiertower.io"
|
|
104
33
|
},
|
|
105
34
|
{
|
|
106
|
-
"key": "
|
|
107
|
-
"value": "
|
|
35
|
+
"key": "Content-Security-Policy",
|
|
36
|
+
"value": "frame-ancestors https://os.frontiertower.io https://sandbox.os.frontiertower.io http://localhost:5173;"
|
|
108
37
|
},
|
|
109
38
|
{
|
|
110
|
-
"key": "
|
|
111
|
-
"value": "
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"source": "/(.*)",
|
|
117
|
-
"has": [
|
|
118
|
-
{
|
|
119
|
-
"type": "header",
|
|
120
|
-
"key": "Origin",
|
|
121
|
-
"value": "http://localhost:5173"
|
|
122
|
-
}
|
|
123
|
-
],
|
|
124
|
-
"headers": [
|
|
125
|
-
{
|
|
126
|
-
"key": "Access-Control-Allow-Origin",
|
|
127
|
-
"value": "http://localhost:5173"
|
|
39
|
+
"key": "X-Content-Type-Options",
|
|
40
|
+
"value": "nosniff"
|
|
128
41
|
},
|
|
129
42
|
{
|
|
130
|
-
"key": "
|
|
131
|
-
"value": "
|
|
43
|
+
"key": "Referrer-Policy",
|
|
44
|
+
"value": "strict-origin-when-cross-origin"
|
|
132
45
|
},
|
|
133
46
|
{
|
|
134
|
-
"key": "
|
|
135
|
-
"value": "
|
|
47
|
+
"key": "Permissions-Policy",
|
|
48
|
+
"value": "camera=(), microphone=(), geolocation=()"
|
|
136
49
|
}
|
|
137
50
|
]
|
|
138
51
|
}
|
|
@@ -140,25 +53,23 @@ All Frontier OS apps deploy to Vercel. The `vercel.json` file is identical acros
|
|
|
140
53
|
}
|
|
141
54
|
```
|
|
142
55
|
|
|
143
|
-
###
|
|
56
|
+
### One Header Block + CSP `frame-ancestors`
|
|
144
57
|
|
|
145
|
-
|
|
58
|
+
A single unconditional header block applies CORS for the production origin plus the security headers to every response. Embedding by all three Frontier origins is granted by the `Content-Security-Policy: frame-ancestors` directive (which lists all three) — `frame-ancestors` is what actually governs iframe embedding, not per-origin CORS reflection. This replaces the older pattern of one `has`-matched CORS block per origin.
|
|
146
59
|
|
|
147
60
|
---
|
|
148
61
|
|
|
149
62
|
## Allowed Origins
|
|
150
63
|
|
|
151
|
-
The Frontier Wallet PWA runs at these
|
|
64
|
+
The Frontier Wallet PWA runs at these 3 origins. Apps must allow all of them to embed the app via the CSP `frame-ancestors` directive:
|
|
152
65
|
|
|
153
66
|
| Origin | Environment | Description |
|
|
154
67
|
| ----------------------------------------- | ------------ | -------------------------------------------------------------- |
|
|
155
68
|
| `http://localhost:5173` | Development | Local Vite dev server for the PWA |
|
|
156
69
|
| `https://sandbox.os.frontiertower.io` | Sandbox | Sandbox environment |
|
|
157
|
-
| `https://alpha.os.frontiertower.io` | Alpha | Early access, design preview -- "there will be dragons" |
|
|
158
|
-
| `https://beta.os.frontiertower.io` | Beta | Internally QA'd and tested, no external audit |
|
|
159
70
|
| `https://os.frontiertower.io` | Production | Production ready |
|
|
160
71
|
|
|
161
|
-
These origins are also hardcoded in the SDK at `@frontiertower/frontier-sdk/ui-utils/detection.ts` as `ALLOWED_ORIGINS
|
|
72
|
+
These origins are also hardcoded in the SDK at `@frontiertower/frontier-sdk/ui-utils/detection.ts` as `ALLOWED_ORIGINS` (exactly these 3). Note: `isInFrontierApp()` no longer consults this list -- it returns `window.self !== window.top` -- so `ALLOWED_ORIGINS` is informational for CORS/CSP only.
|
|
162
73
|
|
|
163
74
|
The `isInFrontierApp()` function checks `window.self !== window.top` to detect if the app is running inside the Frontier Wallet iframe. The `getParentOrigin()` function resolves the parent frame's origin via `document.referrer` or `window.parent.location.origin`.
|
|
164
75
|
|
|
@@ -168,16 +79,16 @@ The `isInFrontierApp()` function checks `window.self !== window.top` to detect i
|
|
|
168
79
|
|
|
169
80
|
## Security Headers
|
|
170
81
|
|
|
171
|
-
|
|
82
|
+
The `vercel.json` above already ships these alongside CORS — every app should keep them:
|
|
172
83
|
|
|
173
|
-
| Header |
|
|
84
|
+
| Header | Value (shipped in vercel.json) | Purpose |
|
|
174
85
|
| ---------------------------- | -------------------------------------------------- | ------------------------------------------- |
|
|
175
|
-
| `Content-Security-Policy` |
|
|
86
|
+
| `Content-Security-Policy` | `frame-ancestors https://os.frontiertower.io https://sandbox.os.frontiertower.io http://localhost:5173;` (the 3 live origins) | Restricts who may embed the app |
|
|
176
87
|
| `X-Content-Type-Options` | `nosniff` | Prevents MIME-type sniffing |
|
|
177
88
|
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Controls referrer information leakage |
|
|
178
|
-
| `Permissions-Policy` |
|
|
89
|
+
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Disables unused browser APIs |
|
|
179
90
|
|
|
180
|
-
The `frame-ancestors` CSP directive is critical because apps load inside the PWA's iframe. Without it, the browser may block the embed.
|
|
91
|
+
The `frame-ancestors` CSP directive is critical because apps load inside the PWA's iframe. Without it, the browser may block the embed. The CSP is intentionally limited to `frame-ancestors` so it never blocks app resources (e.g. the Google Fonts stylesheet the template loads); apps that self-host all assets may tighten it further.
|
|
181
92
|
|
|
182
93
|
---
|
|
183
94
|
|
|
@@ -192,7 +103,7 @@ The recommended way is to use the OS Developer app in the Frontier AppStore. The
|
|
|
192
103
|
1. **Get developer access** -- contact support@frontiertower.io to be added as a developer manager.
|
|
193
104
|
2. **Get your developer profile** -- install the OS Developer app from the AppStore, or call `GET /third-party/developers/`.
|
|
194
105
|
3. **Rotate your API key** immediately after receiving it: `POST /third-party/developers/{developer_id}/rotate-key/`. Store the new key securely.
|
|
195
|
-
4. **Create the app** -- via OS Developer or `POST /third-party/apps/` with
|
|
106
|
+
4. **Create the app** -- via OS Developer (`sdk.getThirdParty().createApp(...)`) or `POST /third-party/apps/` with body `{ url, cnameEntry, txtEntry?, permissions: string[], permissionDisclaimer }` (OS Developer additionally sends `developer: <id>`). App `name`, `description`, and `icon` are NOT sent -- they are auto-fetched from the app URL's HTML metadata (`<title>`, `<meta name="description">`, `<link rel="icon">`).
|
|
196
107
|
|
|
197
108
|
### App Status Lifecycle
|
|
198
109
|
|
|
@@ -217,24 +128,29 @@ When registering, declare the SDK permissions your app requires:
|
|
|
217
128
|
```typescript
|
|
218
129
|
const APP_REGISTRY: AppMetadata[] = [
|
|
219
130
|
{
|
|
220
|
-
id: '
|
|
221
|
-
url: 'https://
|
|
222
|
-
|
|
223
|
-
version: '1.0.0',
|
|
131
|
+
id: 'ifnd-converter',
|
|
132
|
+
url: 'https://ifnd-converter.apps.frontiertower.io',
|
|
133
|
+
icon: '/svgs/ifnd_converter.svg',
|
|
224
134
|
developer: {
|
|
225
135
|
name: 'Developer Name',
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
permissions: {
|
|
229
|
-
wallet: true,
|
|
230
|
-
storage: true,
|
|
231
|
-
notifications: false,
|
|
136
|
+
url: 'https://frontiertower.io',
|
|
137
|
+
description: 'Made with love by the Frontier Tower Action Team',
|
|
232
138
|
},
|
|
139
|
+
permissions: [
|
|
140
|
+
'wallet:getBalance',
|
|
141
|
+
'wallet:getAddress',
|
|
142
|
+
'wallet:executeBatchCall',
|
|
143
|
+
'wallet:executeCall',
|
|
144
|
+
'chain:getCurrentChainConfig',
|
|
145
|
+
],
|
|
146
|
+
permissionDisclaimer:
|
|
147
|
+
'This app accesses your wallet address and balance and executes the conversion calls.',
|
|
148
|
+
// optional: excludedAppStages?, requiresCitizenship?, requiresAdmin?
|
|
233
149
|
} as AppMetadata,
|
|
234
150
|
];
|
|
235
151
|
```
|
|
236
152
|
|
|
237
|
-
Permissions must match
|
|
153
|
+
Permissions are a flat array of `module:method` strings -- each entry must match an SDK method actually called in source code. `name`, `description`, and `icon` are optional in `AppMetadata` (auto-fetched from the app's HTML when omitted). There is no `origin`, `version`, or `developer.verified` field, and there is no `notifications` permission. The fos-verifier agent enforces the permission-to-source match (see [verification-rules.md](verification-rules.md)).
|
|
238
154
|
|
|
239
155
|
---
|
|
240
156
|
|
|
@@ -260,15 +176,15 @@ VITE_API_URL=https://api.example.com
|
|
|
260
176
|
|
|
261
177
|
## DNS Configuration
|
|
262
178
|
|
|
263
|
-
Apps are hosted on the `
|
|
179
|
+
Apps are hosted on the `apps.frontiertower.io` subdomain.
|
|
264
180
|
|
|
265
181
|
### Domain Pattern
|
|
266
182
|
|
|
267
183
|
```
|
|
268
|
-
<app-name>.
|
|
184
|
+
<app-name>.apps.frontiertower.io
|
|
269
185
|
```
|
|
270
186
|
|
|
271
|
-
Example: `kickstarter.
|
|
187
|
+
Example: `kickstarter.apps.frontiertower.io`
|
|
272
188
|
|
|
273
189
|
### DNS Entries
|
|
274
190
|
|
|
@@ -276,12 +192,12 @@ Two DNS records are needed:
|
|
|
276
192
|
|
|
277
193
|
1. **CNAME** -- points the subdomain to the Vercel deployment:
|
|
278
194
|
```
|
|
279
|
-
<app-name>.
|
|
195
|
+
<app-name>.apps.frontiertower.io CNAME cname.vercel-dns.com
|
|
280
196
|
```
|
|
281
197
|
|
|
282
198
|
2. **TXT** -- Vercel domain verification:
|
|
283
199
|
```
|
|
284
|
-
_vercel.<app-name>.
|
|
200
|
+
_vercel.<app-name>.apps.frontiertower.io TXT vc-domain-verify=<token>
|
|
285
201
|
```
|
|
286
202
|
|
|
287
203
|
Configure the custom domain in the Vercel dashboard after DNS propagation. Vercel automatically provisions an SSL certificate.
|
|
@@ -381,7 +297,7 @@ Call `POST /third-party/webhooks/{webhook_id}/rotate-key/`. Deliveries immediate
|
|
|
381
297
|
After deploying, verify CORS is working:
|
|
382
298
|
|
|
383
299
|
```bash
|
|
384
|
-
curl -I https://<app-name>.
|
|
300
|
+
curl -I https://<app-name>.apps.frontiertower.io \
|
|
385
301
|
-H "Origin: https://os.frontiertower.io"
|
|
386
302
|
|
|
387
303
|
# Should include:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SDK Module Index
|
|
2
|
+
|
|
3
|
+
Maps app descriptions to Frontier SDK modules. The CLI (`fos-tools.cjs infer-modules`) does keyword matching programmatically — this file documents the algorithm and serves as an index to per-module reference files.
|
|
4
|
+
|
|
5
|
+
## Inference Algorithm
|
|
6
|
+
|
|
7
|
+
This mirrors `cmdInferModules` in `fos-tools.cjs` exactly:
|
|
8
|
+
|
|
9
|
+
1. Lowercase the entire description (`description.toLowerCase()`) — no tokenization or word splitting.
|
|
10
|
+
2. For each SDK module, substring-match its trigger keywords against the lowercased description with `String.includes()` (keywords are listed in each module's reference file under `references/sdk/`). Because matching is by substring, multi-word phrases like `send money`, `access control`, and `api key` work, and a keyword can match inside a larger word (e.g. `fund` matches within `refund`).
|
|
11
|
+
3. Always include Storage and Chain (the base modules — every app needs these).
|
|
12
|
+
4. Include User if User's own keywords match, or if any of Wallet, Events, Communities, Partnerships, or Offices matched. User is NOT added by ThirdParty, Navigation, Storage, or Chain alone.
|
|
13
|
+
5. Present the inferred modules for user confirmation.
|
|
14
|
+
|
|
15
|
+
## Module Quick Reference
|
|
16
|
+
|
|
17
|
+
| Module | Reference File | Primary Use Case |
|
|
18
|
+
|--------|---------------|-----------------|
|
|
19
|
+
| Wallet | `references/sdk/wallet.md` | Payments, transfers, FND/iFND, fiat on/off-ramp |
|
|
20
|
+
| Storage | `references/sdk/storage.md` | Key-value persistence, preferences |
|
|
21
|
+
| Chain | `references/sdk/chain.md` | Network config, contract addresses |
|
|
22
|
+
| User | `references/sdk/user.md` | Profiles, access controls, membership |
|
|
23
|
+
| Communities | `references/sdk/communities.md` | Groups, internships, member management |
|
|
24
|
+
| Partnerships | `references/sdk/partnerships.md` | Sponsors, passes, brand partnerships |
|
|
25
|
+
| Events | `references/sdk/events.md` | Calendar, rooms, bookings, RSVPs |
|
|
26
|
+
| Offices | `references/sdk/offices.md` | Physical access passes, building entry |
|
|
27
|
+
| ThirdParty | `references/sdk/thirdparty.md` | Developer tools, webhooks, API keys |
|
|
28
|
+
| Navigation | `references/sdk/navigation.md` | Deep links, cross-app navigation |
|
|
29
|
+
|
|
30
|
+
## After Inference
|
|
31
|
+
|
|
32
|
+
Once modules are confirmed, read only the relevant `references/sdk/<module>.md` files for detailed method signatures, types, and permissions. Always include `references/sdk/init.md` and `references/sdk/types.md` as shared context. For any app that reads, displays, transfers, or swaps FND amounts, also read `references/sdk/token-amount.md` — FND amounts are `bigint` base units bridged to/from display strings via `formatAmount()`/`parseAmount()`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Chain Module
|
|
2
|
+
|
|
3
|
+
**Trigger keywords:** network, chain, blockchain, contract, smart contract, on-chain, token address, stablecoin
|
|
4
|
+
|
|
5
|
+
Access via `sdk.getChain()`. Query and switch blockchain networks.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Methods
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
getCurrentNetwork(): Promise<string>
|
|
13
|
+
```
|
|
14
|
+
Returns the current network identifier (e.g. `'base'`, `'base-sepolia'`). Permission: `chain:getCurrentNetwork`
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
getAvailableNetworks(): Promise<string[]>
|
|
18
|
+
```
|
|
19
|
+
Returns all network identifiers the app can switch to. Permission: `chain:getAvailableNetworks`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
switchNetwork(network: string): Promise<void>
|
|
23
|
+
```
|
|
24
|
+
Switch active blockchain network. Affects all subsequent wallet operations. Permission: `chain:switchNetwork`
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
getCurrentChainConfig(): Promise<ChainConfig>
|
|
28
|
+
```
|
|
29
|
+
Returns full chain configuration for the current network. Permission: `chain:getCurrentChainConfig`
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
getContractAddresses(): Promise<{
|
|
33
|
+
fnd: string;
|
|
34
|
+
iFnd: string | null;
|
|
35
|
+
paymentRouter: string;
|
|
36
|
+
subscriptionManager: string;
|
|
37
|
+
}>
|
|
38
|
+
```
|
|
39
|
+
Returns addresses for FND, iFND (may be null), PaymentRouter, and SubscriptionManager contracts on the current chain. Permission: `chain:getContractAddresses`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Types
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
enum Underlying {
|
|
47
|
+
USD = "USD",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Token {
|
|
51
|
+
name: string;
|
|
52
|
+
symbol: string;
|
|
53
|
+
decimals: number;
|
|
54
|
+
address: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface StableCoin extends Token {
|
|
58
|
+
underlying: Underlying;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ChainConfig {
|
|
62
|
+
id: number;
|
|
63
|
+
name: string;
|
|
64
|
+
network: string;
|
|
65
|
+
bridgeSwapRouterFactoryAddress: string;
|
|
66
|
+
uniswapV3FactoryAddress: string;
|
|
67
|
+
nativeCurrency: {
|
|
68
|
+
name: string;
|
|
69
|
+
symbol: string;
|
|
70
|
+
decimals: number;
|
|
71
|
+
};
|
|
72
|
+
blockExplorer: {
|
|
73
|
+
name: string;
|
|
74
|
+
url: string;
|
|
75
|
+
};
|
|
76
|
+
stableCoins: StableCoin[];
|
|
77
|
+
supportedTokens: Token[];
|
|
78
|
+
testnet: boolean;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Permissions (5)
|
|
85
|
+
|
|
86
|
+
| Permission | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `chain:getCurrentNetwork` | Get current network name |
|
|
89
|
+
| `chain:getAvailableNetworks` | Get list of available networks |
|
|
90
|
+
| `chain:switchNetwork` | Switch to a different network |
|
|
91
|
+
| `chain:getCurrentChainConfig` | Get full chain configuration |
|
|
92
|
+
| `chain:getContractAddresses` | Get FND, iFND, PaymentRouter, SubscriptionManager addresses |
|