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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -14
  3. package/agents/fos-executor.md +105 -39
  4. package/agents/fos-plan-checker.md +62 -25
  5. package/agents/fos-planner.md +80 -72
  6. package/agents/fos-researcher.md +26 -15
  7. package/agents/fos-verifier.md +96 -27
  8. package/bin/fos-tools.cjs +146 -42
  9. package/bin/install.js +8 -5
  10. package/commands/fos/add-feature.md +1 -2
  11. package/commands/fos/discuss.md +0 -1
  12. package/commands/fos/new-app.md +2 -4
  13. package/commands/fos/new-milestone.md +1 -1
  14. package/commands/fos/plan.md +0 -2
  15. package/package.json +7 -1
  16. package/references/app-patterns.md +128 -21
  17. package/references/deployment.md +40 -124
  18. package/references/module-index.md +32 -0
  19. package/references/sdk/chain.md +92 -0
  20. package/references/sdk/communities.md +159 -0
  21. package/references/sdk/events.md +212 -0
  22. package/references/sdk/init.md +126 -0
  23. package/references/sdk/navigation.md +49 -0
  24. package/references/sdk/offices.md +76 -0
  25. package/references/sdk/partnerships.md +111 -0
  26. package/references/sdk/storage.md +44 -0
  27. package/references/sdk/thirdparty.md +240 -0
  28. package/references/sdk/token-amount.md +99 -0
  29. package/references/sdk/types.md +27 -0
  30. package/references/sdk/ui-utils.md +39 -0
  31. package/references/sdk/user.md +208 -0
  32. package/references/sdk/wallet.md +334 -0
  33. package/references/verification-rules.md +111 -50
  34. package/templates/app/frontier-services.tsx +871 -0
  35. package/templates/app/layout-standalone.tsx +8 -0
  36. package/templates/app/layout.tsx +19 -9
  37. package/templates/app/package-standalone.json +35 -0
  38. package/templates/app/package.json +2 -1
  39. package/templates/app/public/favicon.svg +3 -0
  40. package/templates/app/sdk-context.tsx +7 -9
  41. package/templates/app/sdk-services.tsx +98 -0
  42. package/templates/app/vercel-standalone.json +5 -0
  43. package/templates/app/vercel.json +8 -95
  44. package/templates/state/plan.md +32 -14
  45. package/templates/state/requirements.md +1 -1
  46. package/templates/state/roadmap.md +57 -24
  47. package/templates/state/summary.md +27 -30
  48. package/workflows/add-feature.md +6 -1
  49. package/workflows/discuss.md +126 -11
  50. package/workflows/execute-plan.md +21 -14
  51. package/workflows/execute.md +204 -24
  52. package/workflows/new-app.md +64 -23
  53. package/workflows/new-milestone.md +10 -3
  54. package/workflows/plan.md +16 -5
  55. package/workflows/ship.md +91 -34
  56. package/workflows/status.md +1 -2
  57. package/references/module-inference.md +0 -349
  58. package/references/sdk-surface.md +0 -1622
  59. package/templates/app/main-simple.tsx +0 -19
  60. 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.15.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
- sdk-context.tsx # SdkProvider + useSdk hook (identical)
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/sdk-context.tsx`
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, useRef, useState, type ReactNode } from 'react';
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 sdkRef = useRef<FrontierSDK | null>(null);
84
- const [ready, setReady] = useState(false);
89
+ const [sdk, setSdk] = useState<FrontierSDK | null>(null);
85
90
 
86
91
  useEffect(() => {
87
- const sdk = new FrontierSDK();
88
- sdkRef.current = sdk;
89
- setReady(true);
92
+ const instance = new FrontierSDK();
93
+ setSdk(instance);
90
94
 
91
95
  return () => {
92
- sdk.destroy();
96
+ instance.destroy();
93
97
  };
94
98
  }, []);
95
99
 
96
- if (!ready) return null;
100
+ if (!sdk) return null;
97
101
 
98
102
  return (
99
- <SdkContext.Provider value={sdkRef.current}>
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 configuration with 5 origin blocks.
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
- <Outlet />
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-Component Variant
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
- For very simple apps that need only one view, the router can be omitted. In this case:
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
@@ -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": "Access-Control-Allow-Methods",
107
- "value": "GET, OPTIONS"
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": "Access-Control-Allow-Headers",
111
- "value": "Content-Type"
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": "Access-Control-Allow-Methods",
131
- "value": "GET, OPTIONS"
43
+ "key": "Referrer-Policy",
44
+ "value": "strict-origin-when-cross-origin"
132
45
  },
133
46
  {
134
- "key": "Access-Control-Allow-Headers",
135
- "value": "Content-Type"
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
- ### Why 5 Separate Blocks
56
+ ### One Header Block + CSP `frame-ancestors`
144
57
 
145
- Vercel does not support multiple values in a single `Access-Control-Allow-Origin` header. Each allowed origin requires its own conditional header block using the `has` matcher. The `has` condition checks the incoming `Origin` request header and only attaches the CORS response headers when the origin matches.
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 5 origins. Apps must allow CORS from all of them:
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
- In addition to CORS, all deployment targets should include:
82
+ The `vercel.json` above already ships these alongside CORS every app should keep them:
172
83
 
173
- | Header | Recommended Value | Purpose |
84
+ | Header | Value (shipped in vercel.json) | Purpose |
174
85
  | ---------------------------- | -------------------------------------------------- | ------------------------------------------- |
175
- | `Content-Security-Policy` | Include `frame-ancestors` for all 5 Frontier origins | Prevents embedding by unauthorized sites |
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` | Disable unnecessary browser APIs | Reduces attack surface |
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 metadata: `name`, `url`, `description`, optional DNS entries.
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: 'my-app',
221
- url: 'https://my-app.appstore.frontiertower.io',
222
- origin: 'https://my-app.appstore.frontiertower.io',
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
- verified: true,
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 the SDK methods actually called in source code. The fos-verifier agent enforces this (see [verification-rules.md](verification-rules.md)).
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 `appstore.frontiertower.io` subdomain.
179
+ Apps are hosted on the `apps.frontiertower.io` subdomain.
264
180
 
265
181
  ### Domain Pattern
266
182
 
267
183
  ```
268
- <app-name>.appstore.frontiertower.io
184
+ <app-name>.apps.frontiertower.io
269
185
  ```
270
186
 
271
- Example: `kickstarter.appstore.frontiertower.io`
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>.appstore.frontiertower.io CNAME cname.vercel-dns.com
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>.appstore.frontiertower.io TXT vc-domain-verify=<token>
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>.appstore.frontiertower.io \
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 |