create-fedi-app 0.1.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 (133) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +11113 -0
  3. package/dist/templates/base/.env.example +5 -0
  4. package/dist/templates/base/app/demo/page.tsx +25 -0
  5. package/dist/templates/base/app/globals.css +95 -0
  6. package/dist/templates/base/app/layout.tsx +39 -0
  7. package/dist/templates/base/app/page.tsx +83 -0
  8. package/dist/templates/base/components/FediDevToolbar/FediDevToolbar.tsx +170 -0
  9. package/dist/templates/base/components/providers.tsx +63 -0
  10. package/dist/templates/base/env.ts +10 -0
  11. package/dist/templates/base/hooks/useFediInternal.ts +41 -0
  12. package/dist/templates/base/lib/fedi-types.ts +96 -0
  13. package/dist/templates/base/lib/fedi.ts +18 -0
  14. package/dist/templates/base/lib/nostr/hooks.ts +52 -0
  15. package/dist/templates/base/lib/nostr/index.ts +9 -0
  16. package/dist/templates/base/lib/nostr/mock.ts +60 -0
  17. package/dist/templates/base/lib/nostr/provider.tsx +64 -0
  18. package/dist/templates/base/lib/utils.ts +3 -0
  19. package/dist/templates/base/lib/webln/hooks.ts +67 -0
  20. package/dist/templates/base/lib/webln/index.ts +12 -0
  21. package/dist/templates/base/lib/webln/mock.ts +96 -0
  22. package/dist/templates/base/lib/webln/provider.tsx +52 -0
  23. package/dist/templates/base/next.config.ts +3 -0
  24. package/dist/templates/base/package.json +40 -0
  25. package/dist/templates/base/proxy.ts +8 -0
  26. package/dist/templates/base/tsconfig.json +20 -0
  27. package/dist/templates/base/vitest.config.ts +6 -0
  28. package/dist/templates/base/vitest.setup.ts +40 -0
  29. package/dist/templates/modules/ai-assistant/app/api/assistant/route.ts +45 -0
  30. package/dist/templates/modules/ai-assistant/app/demo/assistant/AssistantDemoClient.tsx +70 -0
  31. package/dist/templates/modules/ai-assistant/app/demo/assistant/page.tsx +23 -0
  32. package/dist/templates/modules/ai-assistant/components/ai/Assistant.tsx +220 -0
  33. package/dist/templates/modules/ai-assistant/components/ai/AssistantProvider.tsx +71 -0
  34. package/dist/templates/modules/ai-assistant/lib/ai/providers.ts +49 -0
  35. package/dist/templates/modules/ai-assistant/module.json +48 -0
  36. package/dist/templates/modules/ai-chat-gated/app/api/chat/invoice/route.ts +15 -0
  37. package/dist/templates/modules/ai-chat-gated/app/api/chat/route.ts +57 -0
  38. package/dist/templates/modules/ai-chat-gated/app/demo/ai-chat/page.tsx +58 -0
  39. package/dist/templates/modules/ai-chat-gated/components/ai/ChatMessage.tsx +50 -0
  40. package/dist/templates/modules/ai-chat-gated/components/ai/GatedChat.tsx +181 -0
  41. package/dist/templates/modules/ai-chat-gated/components/ai/PaymentGate.tsx +168 -0
  42. package/dist/templates/modules/ai-chat-gated/lib/ai/providers.ts +49 -0
  43. package/dist/templates/modules/ai-chat-gated/lib/chat-payment.ts +161 -0
  44. package/dist/templates/modules/ai-chat-gated/module.json +62 -0
  45. package/dist/templates/modules/ai-rules/.cursorrules +8 -0
  46. package/dist/templates/modules/ai-rules/.github/copilot-instructions.md +8 -0
  47. package/dist/templates/modules/ai-rules/CLAUDE.md +8 -0
  48. package/dist/templates/modules/ai-rules/module.json +20 -0
  49. package/dist/templates/modules/ai-rules/rules/OVERVIEW.md +56 -0
  50. package/dist/templates/modules/ai-rules/rules/architecture.md +108 -0
  51. package/dist/templates/modules/ai-rules/rules/design-system.md +94 -0
  52. package/dist/templates/modules/ai-rules/rules/fedi-api.md +120 -0
  53. package/dist/templates/modules/ai-rules/rules/nostr.md +232 -0
  54. package/dist/templates/modules/ai-rules/rules/patterns.md +408 -0
  55. package/dist/templates/modules/ai-rules/rules/testing.md +238 -0
  56. package/dist/templates/modules/ai-rules/rules/webln.md +241 -0
  57. package/dist/templates/modules/database/drizzle/supabase/0000_initial.sql +7 -0
  58. package/dist/templates/modules/database/drizzle/supabase/meta/_journal.json +13 -0
  59. package/dist/templates/modules/database/drizzle/turso/0000_initial.sql +7 -0
  60. package/dist/templates/modules/database/drizzle/turso/meta/_journal.json +13 -0
  61. package/dist/templates/modules/database/drizzle.config.supabase.ts +10 -0
  62. package/dist/templates/modules/database/drizzle.config.turso.ts +11 -0
  63. package/dist/templates/modules/database/env.supabase.ts +24 -0
  64. package/dist/templates/modules/database/env.turso.ts +23 -0
  65. package/dist/templates/modules/database/lib/db/index.supabase.ts +19 -0
  66. package/dist/templates/modules/database/lib/db/index.turso.ts +20 -0
  67. package/dist/templates/modules/database/lib/db/schema.supabase.ts +13 -0
  68. package/dist/templates/modules/database/lib/db/schema.turso.ts +13 -0
  69. package/dist/templates/modules/database/module.json +110 -0
  70. package/dist/templates/modules/ecash-balance/app/demo/ecash/page.tsx +115 -0
  71. package/dist/templates/modules/ecash-balance/components/fedi/BalanceDisplay.tsx +162 -0
  72. package/dist/templates/modules/ecash-balance/components/fedi/FediVersionBadge.tsx +39 -0
  73. package/dist/templates/modules/ecash-balance/components/fedi/InstallMiniAppButton.tsx +74 -0
  74. package/dist/templates/modules/ecash-balance/hooks/useFediBalance.ts +65 -0
  75. package/dist/templates/modules/ecash-balance/module.json +14 -0
  76. package/dist/templates/modules/lnurl/app/api/lnurlauth/route.ts +118 -0
  77. package/dist/templates/modules/lnurl/app/api/lnurlp/[username]/route.ts +70 -0
  78. package/dist/templates/modules/lnurl/app/api/lnurlw/route.ts +57 -0
  79. package/dist/templates/modules/lnurl/app/demo/lnurl/page.tsx +136 -0
  80. package/dist/templates/modules/lnurl/components/lnurl/LnurlAuth.tsx +156 -0
  81. package/dist/templates/modules/lnurl/components/lnurl/LnurlPay.tsx +36 -0
  82. package/dist/templates/modules/lnurl/components/lnurl/LnurlQR.tsx +96 -0
  83. package/dist/templates/modules/lnurl/components/lnurl/LnurlWithdraw.tsx +141 -0
  84. package/dist/templates/modules/lnurl/lib/lnurl-auth-verify.ts +87 -0
  85. package/dist/templates/modules/lnurl/lib/lnurl-store.ts +112 -0
  86. package/dist/templates/modules/lnurl/lib/lnurl-utils.ts +56 -0
  87. package/dist/templates/modules/lnurl/module.json +27 -0
  88. package/dist/templates/modules/multispend-demo/app/demo/multispend/MultispendDemoClient.tsx +109 -0
  89. package/dist/templates/modules/multispend-demo/app/demo/multispend/page.tsx +23 -0
  90. package/dist/templates/modules/multispend-demo/components/multispend/ApprovalVote.tsx +122 -0
  91. package/dist/templates/modules/multispend-demo/components/multispend/MultispendDemo.tsx +220 -0
  92. package/dist/templates/modules/multispend-demo/components/multispend/MultispendProposal.tsx +213 -0
  93. package/dist/templates/modules/multispend-demo/components/multispend/ProposalList.tsx +49 -0
  94. package/dist/templates/modules/multispend-demo/hooks/useMultispendDemo.ts +127 -0
  95. package/dist/templates/modules/multispend-demo/lib/multispend-types.ts +33 -0
  96. package/dist/templates/modules/multispend-demo/lib/multispend-utils.ts +69 -0
  97. package/dist/templates/modules/multispend-demo/module.json +18 -0
  98. package/dist/templates/modules/nostr-feed/app/demo/nostr-feed/NostrFeedDemoClient.tsx +134 -0
  99. package/dist/templates/modules/nostr-feed/app/demo/nostr-feed/page.tsx +23 -0
  100. package/dist/templates/modules/nostr-feed/components/nostr/NostrFeedProvider.tsx +47 -0
  101. package/dist/templates/modules/nostr-feed/components/nostr/NoteCard.tsx +68 -0
  102. package/dist/templates/modules/nostr-feed/components/nostr/NoteFeed.tsx +109 -0
  103. package/dist/templates/modules/nostr-feed/components/nostr/PublishNote.tsx +104 -0
  104. package/dist/templates/modules/nostr-feed/components/nostr/ZapButton.tsx +140 -0
  105. package/dist/templates/modules/nostr-feed/lib/nostr/relay.ts +107 -0
  106. package/dist/templates/modules/nostr-feed/lib/nostr-zap.ts +159 -0
  107. package/dist/templates/modules/nostr-feed/module.json +25 -0
  108. package/dist/templates/modules/nostr-identity/app/demo/nostr/page.tsx +136 -0
  109. package/dist/templates/modules/nostr-identity/components/nostr/IdentityBadge.tsx +109 -0
  110. package/dist/templates/modules/nostr-identity/components/nostr/NostrLogin.tsx +107 -0
  111. package/dist/templates/modules/nostr-identity/components/nostr/SignedMessage.tsx +103 -0
  112. package/dist/templates/modules/nostr-identity/hooks/useIdentityFlow.ts +61 -0
  113. package/dist/templates/modules/nostr-identity/lib/nostr-utils.ts +30 -0
  114. package/dist/templates/modules/nostr-identity/module.json +15 -0
  115. package/dist/templates/modules/payment-gated-content/app/api/payment-gate/invoice/route.ts +25 -0
  116. package/dist/templates/modules/payment-gated-content/app/api/payment-gate/verify/route.ts +39 -0
  117. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/article/page.tsx +71 -0
  118. package/dist/templates/modules/payment-gated-content/app/demo/payment-gated/page.tsx +134 -0
  119. package/dist/templates/modules/payment-gated-content/components/payment-gated/PayGate.tsx +267 -0
  120. package/dist/templates/modules/payment-gated-content/lib/payment-gate.ts +195 -0
  121. package/dist/templates/modules/payment-gated-content/lib/payment-store.ts +104 -0
  122. package/dist/templates/modules/payment-gated-content/module.json +24 -0
  123. package/dist/templates/modules/payment-gated-content/proxy.ts +27 -0
  124. package/dist/templates/modules/webln-payments/app/demo/webln/page.tsx +176 -0
  125. package/dist/templates/modules/webln-payments/components/webln/InvoiceCard.tsx +170 -0
  126. package/dist/templates/modules/webln-payments/components/webln/PayButton.tsx +92 -0
  127. package/dist/templates/modules/webln-payments/components/webln/PaymentHistory.tsx +102 -0
  128. package/dist/templates/modules/webln-payments/hooks/__tests__/usePaymentFlow.test.tsx +182 -0
  129. package/dist/templates/modules/webln-payments/hooks/usePaymentFlow.ts +100 -0
  130. package/dist/templates/modules/webln-payments/lib/payment-history.ts +75 -0
  131. package/dist/templates/modules/webln-payments/module.json +17 -0
  132. package/dist/templates/modules/webln-payments/tests/e2e/webln-payment.spec.ts +41 -0
  133. package/package.json +29 -0
@@ -0,0 +1,52 @@
1
+ import { useContext, useState } from 'react';
2
+ import { bech32 } from '@scure/base';
3
+ import { NostrContext } from './provider';
4
+ import type { UnsignedNostrEvent, NostrEvent } from '../fedi-types';
5
+
6
+ function pubkeyToNpub(hex: string): string {
7
+ const bytes = new Uint8Array(hex.length / 2);
8
+ for (let i = 0; i < hex.length; i += 2) {
9
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
10
+ }
11
+ return bech32.encodeFromBytes('npub', bytes);
12
+ }
13
+
14
+ export function useNostr() {
15
+ const ctx = useContext(NostrContext);
16
+ if (ctx === null) {
17
+ throw new Error('useNostr must be used within a NostrProvider');
18
+ }
19
+ const npub = ctx.pubkey ? pubkeyToNpub(ctx.pubkey) : null;
20
+ return {
21
+ provider: ctx.provider,
22
+ pubkey: ctx.pubkey,
23
+ npub,
24
+ isLoading: ctx.isLoading,
25
+ error: ctx.error,
26
+ isConnected: ctx.provider !== null,
27
+ };
28
+ }
29
+
30
+ export function useIdentity() {
31
+ const { provider, pubkey, npub } = useNostr();
32
+ const [isConnecting, setIsConnecting] = useState(false);
33
+
34
+ const displayNpub = npub ? npub.slice(0, 8) + '...' + npub.slice(-4) : null;
35
+
36
+ async function getPublicKey(): Promise<string | null> {
37
+ if (!provider) return null;
38
+ return provider.getPublicKey();
39
+ }
40
+
41
+ async function signEvent(event: UnsignedNostrEvent): Promise<NostrEvent | null> {
42
+ if (!provider) return null;
43
+ setIsConnecting(true);
44
+ try {
45
+ return await provider.signEvent(event);
46
+ } finally {
47
+ setIsConnecting(false);
48
+ }
49
+ }
50
+
51
+ return { pubkey, npub, displayNpub, getPublicKey, signEvent, isConnecting };
52
+ }
@@ -0,0 +1,9 @@
1
+ export type {
2
+ NostrEvent,
3
+ UnsignedNostrEvent,
4
+ Nip04,
5
+ } from '../fedi-types';
6
+
7
+ export { NostrContext, NostrProvider } from './provider';
8
+ export { MockNostrProvider } from './mock';
9
+ export { useNostr, useIdentity } from './hooks';
@@ -0,0 +1,60 @@
1
+ // WARNING: Test keypair only. NEVER use in production
2
+ import { schnorr } from '@noble/curves/secp256k1.js';
3
+ import { sha256 } from '@noble/hashes/sha256';
4
+ import type { NostrProvider, NostrEvent, UnsignedNostrEvent, Nip04 } from '../fedi-types';
5
+
6
+ function bytesToHex(bytes: Uint8Array): string {
7
+ return Array.from(bytes)
8
+ .map((b) => b.toString(16).padStart(2, '0'))
9
+ .join('');
10
+ }
11
+
12
+ // Lowest valid secp256k1 private key, famous test vector, NOT for production
13
+ const TEST_PRIVKEY = new Uint8Array(32);
14
+ TEST_PRIVKEY[31] = 1;
15
+
16
+ const TEST_PUBKEY_BYTES = schnorr.getPublicKey(TEST_PRIVKEY);
17
+ const TEST_PUBKEY_HEX = bytesToHex(TEST_PUBKEY_BYTES);
18
+
19
+ export class MockNostrProvider implements NostrProvider {
20
+ readonly nip04: Nip04 = {
21
+ async encrypt(_pubkey: string, plaintext: string): Promise<string> {
22
+ return Buffer.from(plaintext).toString('base64') + '?iv=fakefakefakefake';
23
+ },
24
+ async decrypt(_pubkey: string, ciphertext: string): Promise<string> {
25
+ const [b64] = ciphertext.split('?iv=');
26
+ return Buffer.from(b64, 'base64').toString('utf-8');
27
+ },
28
+ };
29
+
30
+ async getPublicKey(): Promise<string> {
31
+ return TEST_PUBKEY_HEX;
32
+ }
33
+
34
+ async signEvent(event: UnsignedNostrEvent): Promise<NostrEvent> {
35
+ const serialized = JSON.stringify([
36
+ 0,
37
+ TEST_PUBKEY_HEX,
38
+ event.created_at,
39
+ event.kind,
40
+ event.tags,
41
+ event.content,
42
+ ]);
43
+ const idBytes = sha256(new TextEncoder().encode(serialized));
44
+ const idHex = bytesToHex(idBytes);
45
+ const sigBytes = schnorr.sign(idBytes, TEST_PRIVKEY);
46
+ return {
47
+ ...event,
48
+ pubkey: TEST_PUBKEY_HEX,
49
+ id: idHex,
50
+ sig: bytesToHex(sigBytes),
51
+ };
52
+ }
53
+
54
+ async getRelays(): Promise<Record<string, { read: boolean; write: boolean }>> {
55
+ return {
56
+ 'wss://relay.damus.io': { read: true, write: true },
57
+ 'wss://nos.lol': { read: true, write: false },
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,64 @@
1
+ import { createContext, useEffect, useState, type ReactNode } from 'react';
2
+ import type { NostrProvider as NostrProviderInterface } from '../fedi-types';
3
+
4
+ interface NostrContextValue {
5
+ provider: NostrProviderInterface | null;
6
+ pubkey: string | null;
7
+ isLoading: boolean;
8
+ error: Error | null;
9
+ }
10
+
11
+ export const NostrContext = createContext<NostrContextValue | null>(null);
12
+
13
+ interface NostrProviderProps {
14
+ children: ReactNode;
15
+ mockProvider?: NostrProviderInterface;
16
+ }
17
+
18
+ export function NostrProvider({ children, mockProvider }: NostrProviderProps) {
19
+ const [provider, setProvider] = useState<NostrProviderInterface | null>(null);
20
+ const [pubkey, setPubkey] = useState<string | null>(null);
21
+ const [isLoading, setIsLoading] = useState(true);
22
+ const [error, setError] = useState<Error | null>(null);
23
+
24
+ useEffect(() => {
25
+ let cancelled = false;
26
+
27
+ async function init() {
28
+ let activeProvider: NostrProviderInterface | null = null;
29
+
30
+ if (typeof window !== 'undefined' && window.nostr) {
31
+ activeProvider = window.nostr;
32
+ } else if (mockProvider !== undefined && process.env.NODE_ENV === 'development') {
33
+ activeProvider = mockProvider;
34
+ }
35
+
36
+ if (activeProvider) {
37
+ try {
38
+ const pk = await activeProvider.getPublicKey();
39
+ if (!cancelled) {
40
+ setProvider(activeProvider);
41
+ setPubkey(pk);
42
+ }
43
+ } catch (err) {
44
+ if (!cancelled) {
45
+ setError(err instanceof Error ? err : new Error('Failed to connect Nostr'));
46
+ }
47
+ }
48
+ }
49
+
50
+ if (!cancelled) setIsLoading(false);
51
+ }
52
+
53
+ init();
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ }, [mockProvider]);
58
+
59
+ return (
60
+ <NostrContext.Provider value={{ provider, pubkey, isLoading, error }}>
61
+ {children}
62
+ </NostrContext.Provider>
63
+ );
64
+ }
@@ -0,0 +1,3 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
@@ -0,0 +1,67 @@
1
+ import { useContext, useState } from 'react';
2
+ import { WebLNContext } from './provider';
3
+ import type { RequestInvoiceArgs } from '../fedi-types';
4
+
5
+ export function useWebLN() {
6
+ const ctx = useContext(WebLNContext);
7
+ if (ctx === null) {
8
+ throw new Error('useWebLN must be used within a WebLNProvider');
9
+ }
10
+ return {
11
+ provider: ctx.provider,
12
+ isLoading: ctx.isLoading,
13
+ error: ctx.error,
14
+ isConnected: ctx.provider !== null,
15
+ };
16
+ }
17
+
18
+ export function usePayment() {
19
+ const { provider } = useWebLN();
20
+ const [isPaying, setIsPaying] = useState(false);
21
+ const [isCreatingInvoice, setIsCreatingInvoice] = useState(false);
22
+ const [paymentError, setPaymentError] = useState<Error | null>(null);
23
+ const [lastPreimage, setLastPreimage] = useState<string | null>(null);
24
+ const [lastInvoice, setLastInvoice] = useState<string | null>(null);
25
+
26
+ async function sendPayment(paymentRequest: string) {
27
+ if (!provider) return null;
28
+ setIsPaying(true);
29
+ setPaymentError(null);
30
+ try {
31
+ const result = await provider.sendPayment(paymentRequest);
32
+ setLastPreimage(result.preimage);
33
+ return result;
34
+ } catch (err) {
35
+ setPaymentError(err instanceof Error ? err : new Error('Payment failed'));
36
+ return null;
37
+ } finally {
38
+ setIsPaying(false);
39
+ }
40
+ }
41
+
42
+ async function makeInvoice(args: RequestInvoiceArgs | string | number) {
43
+ if (!provider) return null;
44
+ setIsCreatingInvoice(true);
45
+ setPaymentError(null);
46
+ try {
47
+ const result = await provider.makeInvoice(args);
48
+ setLastInvoice(result.paymentRequest);
49
+ return result;
50
+ } catch (err) {
51
+ setPaymentError(err instanceof Error ? err : new Error('Failed to create invoice'));
52
+ return null;
53
+ } finally {
54
+ setIsCreatingInvoice(false);
55
+ }
56
+ }
57
+
58
+ return {
59
+ sendPayment,
60
+ makeInvoice,
61
+ isPaying,
62
+ isCreatingInvoice,
63
+ paymentError,
64
+ lastPreimage,
65
+ lastInvoice,
66
+ };
67
+ }
@@ -0,0 +1,12 @@
1
+ export type {
2
+ RequestInvoiceArgs,
3
+ RequestInvoiceResponse,
4
+ SendPaymentResponse,
5
+ KeysendArgs,
6
+ SignMessageResponse,
7
+ GetInfoResponse,
8
+ } from '../fedi-types';
9
+
10
+ export { WebLNContext, WebLNProvider } from './provider';
11
+ export { MockWebLNProvider } from './mock';
12
+ export { useWebLN, usePayment } from './hooks';
@@ -0,0 +1,96 @@
1
+ import type {
2
+ WebLNProvider,
3
+ GetInfoResponse,
4
+ RequestInvoiceArgs,
5
+ RequestInvoiceResponse,
6
+ SendPaymentResponse,
7
+ KeysendArgs,
8
+ SignMessageResponse,
9
+ } from '../fedi-types';
10
+
11
+ function randomHex(bytes: number): string {
12
+ let result = '';
13
+ for (let i = 0; i < bytes * 2; i++) {
14
+ result += Math.floor(Math.random() * 16).toString(16);
15
+ }
16
+ return result;
17
+ }
18
+
19
+ function randomAlphanumeric(length: number): string {
20
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
21
+ let result = '';
22
+ for (let i = 0; i < length; i++) {
23
+ result += chars[Math.floor(Math.random() * chars.length)];
24
+ }
25
+ return result;
26
+ }
27
+
28
+ function delay(ms: number): Promise<void> {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ interface MockWebLNOptions {
33
+ paymentDelay?: number;
34
+ shouldFail?: boolean;
35
+ failureMessage?: string;
36
+ autoEnable?: boolean;
37
+ }
38
+
39
+ export class MockWebLNProvider implements WebLNProvider {
40
+ private readonly paymentDelay: number;
41
+ private readonly shouldFail: boolean;
42
+ private readonly failureMessage: string;
43
+
44
+ constructor({
45
+ paymentDelay = 600,
46
+ shouldFail = false,
47
+ failureMessage,
48
+ autoEnable: _autoEnable = true,
49
+ }: MockWebLNOptions = {}) {
50
+ this.paymentDelay = paymentDelay;
51
+ this.shouldFail = shouldFail;
52
+ this.failureMessage = failureMessage ?? 'Payment failed';
53
+ }
54
+
55
+ async enable(): Promise<void> {
56
+ if (this.shouldFail) throw new Error(this.failureMessage);
57
+ }
58
+
59
+ async getInfo(): Promise<GetInfoResponse> {
60
+ return {
61
+ node: {
62
+ alias: 'Fedi Dev Node',
63
+ pubkey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
64
+ color: '#FF6B35',
65
+ },
66
+ methods: ['sendPayment', 'makeInvoice', 'getInfo', 'signMessage'],
67
+ };
68
+ }
69
+
70
+ async makeInvoice(args: RequestInvoiceArgs | string | number): Promise<RequestInvoiceResponse> {
71
+ const amount = typeof args === 'object' ? (args.amount ?? args.defaultAmount ?? 0) : args;
72
+ await delay(this.paymentDelay);
73
+ return {
74
+ paymentRequest: `lnbc${amount}n1p${randomAlphanumeric(100)}`,
75
+ };
76
+ }
77
+
78
+ async sendPayment(paymentRequest: string): Promise<SendPaymentResponse> {
79
+ if (!paymentRequest.startsWith('lnbc')) {
80
+ throw new Error('Invalid payment request: must start with lnbc');
81
+ }
82
+ if (this.shouldFail) throw new Error(this.failureMessage);
83
+ await delay(this.paymentDelay);
84
+ return { preimage: randomHex(32) };
85
+ }
86
+
87
+ async signMessage(message: string): Promise<SignMessageResponse> {
88
+ return { message, signature: randomHex(64) };
89
+ }
90
+
91
+ async verifyMessage(): Promise<void> {}
92
+
93
+ async sendKeysend(_args: KeysendArgs): Promise<SendPaymentResponse> {
94
+ return { preimage: randomHex(32) };
95
+ }
96
+ }
@@ -0,0 +1,52 @@
1
+ import { createContext, useEffect, useState, type ReactNode } from 'react';
2
+ import type { WebLNProvider as WebLNProviderInterface } from '../fedi-types';
3
+
4
+ interface WebLNContextValue {
5
+ provider: WebLNProviderInterface | null;
6
+ isLoading: boolean;
7
+ error: Error | null;
8
+ }
9
+
10
+ export const WebLNContext = createContext<WebLNContextValue | null>(null);
11
+
12
+ interface WebLNProviderProps {
13
+ children: ReactNode;
14
+ mockProvider?: WebLNProviderInterface;
15
+ }
16
+
17
+ export function WebLNProvider({ children, mockProvider }: WebLNProviderProps) {
18
+ const [provider, setProvider] = useState<WebLNProviderInterface | null>(null);
19
+ const [isLoading, setIsLoading] = useState(true);
20
+ const [error, setError] = useState<Error | null>(null);
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+
25
+ async function init() {
26
+ if (typeof window !== 'undefined' && window.webln) {
27
+ try {
28
+ await window.webln.enable();
29
+ if (!cancelled) setProvider(window.webln);
30
+ } catch (err) {
31
+ if (!cancelled) {
32
+ setError(err instanceof Error ? err : new Error('Failed to enable WebLN'));
33
+ }
34
+ }
35
+ } else if (mockProvider !== undefined && process.env.NODE_ENV !== 'production') {
36
+ if (!cancelled) setProvider(mockProvider);
37
+ }
38
+ if (!cancelled) setIsLoading(false);
39
+ }
40
+
41
+ init();
42
+ return () => {
43
+ cancelled = true;
44
+ };
45
+ }, [mockProvider]);
46
+
47
+ return (
48
+ <WebLNContext.Provider value={{ provider, isLoading, error }}>
49
+ {children}
50
+ </WebLNContext.Provider>
51
+ );
52
+ }
@@ -0,0 +1,3 @@
1
+ import type { NextConfig } from 'next';
2
+ const config: NextConfig = {};
3
+ export default config;
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "typecheck": "tsc --noEmit",
10
+ "test": "vitest",
11
+ "test:e2e": "playwright test"
12
+ },
13
+ "dependencies": {
14
+ "next": "^16.2.6",
15
+ "react": "^19",
16
+ "react-dom": "^19",
17
+ "@noble/curves": "^2.2.0",
18
+ "@noble/hashes": "^1.7.1",
19
+ "@scure/base": "^2.2.0",
20
+ "@t3-oss/env-nextjs": "^0.12",
21
+ "zod": "^3",
22
+ "clsx": "^2",
23
+ "tailwind-merge": "^2",
24
+ "qrcode.react": "^4"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5",
28
+ "@types/react": "^19",
29
+ "@types/node": "^22",
30
+ "tailwindcss": "^4",
31
+ "@tailwindcss/typography": "^0.5",
32
+ "vitest": "^2",
33
+ "jsdom": "^26",
34
+ "@testing-library/react": "^16",
35
+ "@testing-library/jest-dom": "^6",
36
+ "@testing-library/user-event": "^14",
37
+ "@vitejs/plugin-react": "^4",
38
+ "@playwright/test": "^1.45"
39
+ }
40
+ }
@@ -0,0 +1,8 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ export default function proxy(request: NextRequest) {
3
+ // Base: pass-through. payment-gated-content module extends this.
4
+ return NextResponse.next();
5
+ }
6
+ export const config = {
7
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
8
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "ESNext",
5
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "allowImportingTsExtensions": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }]
17
+ },
18
+ "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
19
+ "exclude": ["node_modules"]
20
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import react from '@vitejs/plugin-react';
3
+ export default defineConfig({
4
+ plugins: [react()],
5
+ test: { environment: 'jsdom', globals: true, setupFiles: ['./vitest.setup.ts'] },
6
+ });
@@ -0,0 +1,40 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ /** Node's experimental localStorage lacks Storage methods. Use an in-memory store in tests. */
4
+ class MemoryStorage implements Storage {
5
+ private store = new Map<string, string>();
6
+
7
+ get length(): number {
8
+ return this.store.size;
9
+ }
10
+
11
+ clear(): void {
12
+ this.store.clear();
13
+ }
14
+
15
+ getItem(key: string): string | null {
16
+ return this.store.get(key) ?? null;
17
+ }
18
+
19
+ key(index: number): string | null {
20
+ return [...this.store.keys()][index] ?? null;
21
+ }
22
+
23
+ removeItem(key: string): void {
24
+ this.store.delete(key);
25
+ }
26
+
27
+ setItem(key: string, value: string): void {
28
+ this.store.set(key, value);
29
+ }
30
+ }
31
+
32
+ if (typeof window !== 'undefined') {
33
+ const storage = window.localStorage;
34
+ if (!storage || typeof storage.setItem !== 'function') {
35
+ Object.defineProperty(window, 'localStorage', {
36
+ value: new MemoryStorage(),
37
+ writable: true,
38
+ });
39
+ }
40
+ }
@@ -0,0 +1,45 @@
1
+ import { convertToModelMessages, streamText, type UIMessage } from 'ai';
2
+ import { getLanguageModel } from '../../../lib/ai/providers';
3
+
4
+ export const maxDuration = 60;
5
+
6
+ const DEFAULT_SYSTEM_PROMPT =
7
+ 'You are a helpful assistant inside a Fedi Mini App. Keep answers concise and practical.';
8
+
9
+ type TAssistantRequestBody = {
10
+ messages?: UIMessage[];
11
+ systemPrompt?: string;
12
+ };
13
+
14
+ /**
15
+ * Streaming AI assistant endpoint. No payment gating, available to all users.
16
+ */
17
+ export async function POST(request: Request) {
18
+ let body: TAssistantRequestBody;
19
+
20
+ try {
21
+ body = (await request.json()) as TAssistantRequestBody;
22
+ } catch {
23
+ return new Response('Invalid JSON body', { status: 400 });
24
+ }
25
+
26
+ const messages = body.messages;
27
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
28
+ return new Response('messages array is required', { status: 400 });
29
+ }
30
+
31
+ const system = body.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
32
+
33
+ try {
34
+ const result = streamText({
35
+ model: getLanguageModel(),
36
+ messages: await convertToModelMessages(messages),
37
+ system,
38
+ });
39
+
40
+ return result.toUIMessageStreamResponse();
41
+ } catch (err) {
42
+ const message = err instanceof Error ? err.message : 'AI request failed';
43
+ return new Response(message, { status: 500 });
44
+ }
45
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { Assistant } from '../../../components/ai/Assistant';
6
+ import { AssistantProvider } from '../../../components/ai/AssistantProvider';
7
+
8
+ const DEFAULT_SYSTEM_PROMPT =
9
+ 'You are a helpful assistant inside a Fedi Mini App. Keep answers concise and practical.';
10
+
11
+ export function AssistantDemoClient() {
12
+ const [systemPrompt, setSystemPrompt] = useState(DEFAULT_SYSTEM_PROMPT);
13
+
14
+ return (
15
+ <div className="min-h-dvh bg-[var(--color-bg)] font-[family-name:var(--font-body)] text-[var(--color-text)]">
16
+ <div
17
+ className="mx-auto w-full max-w-[390px] px-4 pt-6"
18
+ style={{ paddingBottom: 'max(5rem, env(safe-area-inset-bottom, 20px))' }}
19
+ >
20
+ <Link
21
+ href="/demo"
22
+ className="mb-6 inline-block text-xs text-[var(--color-text-muted)] transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] hover:opacity-80"
23
+ >
24
+ ← back
25
+ </Link>
26
+
27
+ <header className="mb-8 space-y-2">
28
+ <h1 className="font-[family-name:var(--font-display)] text-2xl font-bold leading-tight text-[var(--color-text)]">
29
+ AI assistant
30
+ </h1>
31
+ <p className="max-w-[75ch] text-sm leading-[1.65] text-[var(--color-text-muted)]">
32
+ Free chat powered by the Vercel AI SDK. Configure the system prompt below. It is sent
33
+ with every request to <code className="font-mono text-xs">/api/assistant</code> via{' '}
34
+ <code className="font-mono text-xs">streamText()</code>.
35
+ </p>
36
+ </header>
37
+
38
+ <div className="mb-6 space-y-2">
39
+ <label
40
+ htmlFor="system-prompt"
41
+ className="block text-xs font-semibold text-[var(--color-text-muted)]"
42
+ >
43
+ System prompt
44
+ </label>
45
+ <textarea
46
+ id="system-prompt"
47
+ value={systemPrompt}
48
+ onChange={(event) => setSystemPrompt(event.target.value)}
49
+ rows={4}
50
+ className="w-full resize-y rounded-lg px-3 py-2.5 font-mono text-xs leading-[1.65] outline-none"
51
+ style={{
52
+ background: 'var(--color-surface-2)',
53
+ color: 'var(--color-text)',
54
+ border: '1px solid var(--color-border)',
55
+ borderRadius: 'var(--radius-md)',
56
+ }}
57
+ />
58
+ <p className="text-xs leading-[1.65] text-[var(--color-text-subtle)]">
59
+ Changing the prompt starts a fresh context on the next message. Clear the chat if you
60
+ want a clean thread.
61
+ </p>
62
+ </div>
63
+
64
+ <AssistantProvider systemPrompt={systemPrompt}>
65
+ <Assistant />
66
+ </AssistantProvider>
67
+ </div>
68
+ </div>
69
+ );
70
+ }