@frak-labs/frame-connector 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +837 -0
- package/dist/index.d.ts +837 -0
- package/dist/index.js +1 -0
- package/dist/middleware.cjs +1 -0
- package/dist/middleware.d.cts +251 -0
- package/dist/middleware.d.ts +251 -0
- package/dist/middleware.js +1 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @frak-labs/frame-connector
|
|
2
|
+
|
|
3
|
+
Modern, type-safe RPC communication layer for cross-window postMessage communication.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides a robust RPC system for communication between the Frak Wallet and SDK clients. It maintains 100% backward compatibility with the existing message format while providing a modern API with promises and async iterators.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Type-safe**: Full TypeScript support with schema-based typing
|
|
12
|
+
- **Backward compatible**: Uses the existing `{ id, topic, data }` message format
|
|
13
|
+
- **Modern API**: Promises for one-shot requests, async iterators for streams
|
|
14
|
+
- **Functional**: No classes, just pure functions
|
|
15
|
+
- **Secure**: Origin validation and error handling built-in
|
|
16
|
+
- **Framework-agnostic**: Works with any transport mechanism
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
### Message Format (Unchanged)
|
|
21
|
+
|
|
22
|
+
Messages maintain the exact same format for backward compatibility:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
{
|
|
26
|
+
id: string // Unique request identifier
|
|
27
|
+
topic: string // Method name (e.g., 'frak_sendInteraction')
|
|
28
|
+
data: unknown // Request parameters or response data
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Response Types
|
|
33
|
+
|
|
34
|
+
Each RPC method in the schema is annotated with a `ResponseType`:
|
|
35
|
+
|
|
36
|
+
- `"promise"`: One-shot request that resolves once (e.g., `frak_sendInteraction`)
|
|
37
|
+
- `"stream"`: Streaming request that can emit multiple values (e.g., `frak_listenToWalletStatus`)
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Client-Side (SDK)
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createRpcClient } from '@frak-labs/frame-connector'
|
|
45
|
+
|
|
46
|
+
// Create the client
|
|
47
|
+
const client = createRpcClient({
|
|
48
|
+
transport: window,
|
|
49
|
+
targetOrigin: 'https://wallet.frak.id'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Connect before making requests
|
|
53
|
+
await client.connect()
|
|
54
|
+
|
|
55
|
+
// One-shot request (promise-based)
|
|
56
|
+
const result = await client.request(
|
|
57
|
+
'frak_sendInteraction',
|
|
58
|
+
productId,
|
|
59
|
+
interaction,
|
|
60
|
+
signature
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Streaming request (async iterator)
|
|
64
|
+
for await (const status of client.stream('frak_listenToWalletStatus')) {
|
|
65
|
+
console.log('Wallet status:', status)
|
|
66
|
+
|
|
67
|
+
if (status.key === 'connected') {
|
|
68
|
+
console.log('Connected to wallet:', status.wallet)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Cleanup when done
|
|
73
|
+
client.cleanup()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Wallet-Side (Listener)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { createRpcListener } from '@frak-labs/frame-connector'
|
|
80
|
+
|
|
81
|
+
// Create the listener
|
|
82
|
+
const listener = createRpcListener({
|
|
83
|
+
transport: window,
|
|
84
|
+
allowedOrigins: ['https://example.com', 'https://app.example.com']
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Register a promise handler (one-shot)
|
|
88
|
+
listener.handle('frak_sendInteraction', async (params, context) => {
|
|
89
|
+
const [productId, interaction, signature] = params
|
|
90
|
+
|
|
91
|
+
// Process the interaction
|
|
92
|
+
const hash = await processInteraction(productId, interaction, signature)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
status: 'success',
|
|
96
|
+
hash
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Register a stream handler (multiple emissions)
|
|
101
|
+
listener.handleStream('frak_listenToWalletStatus', (params, emit, context) => {
|
|
102
|
+
// Emit initial status
|
|
103
|
+
emit({ key: 'connecting' })
|
|
104
|
+
|
|
105
|
+
// Set up listener for wallet changes
|
|
106
|
+
const unsubscribe = walletState.subscribe((state) => {
|
|
107
|
+
if (state.connected) {
|
|
108
|
+
emit({ key: 'connected', wallet: state.address })
|
|
109
|
+
} else {
|
|
110
|
+
emit({ key: 'not-connected' })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Return cleanup function (optional)
|
|
115
|
+
return () => unsubscribe()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Cleanup when done
|
|
119
|
+
listener.cleanup()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### Client
|
|
125
|
+
|
|
126
|
+
#### `createRpcClient(config: RpcClientConfig): RpcClient`
|
|
127
|
+
|
|
128
|
+
Creates an RPC client for SDK-side communication.
|
|
129
|
+
|
|
130
|
+
**Config:**
|
|
131
|
+
- `transport: RpcTransport` - The transport mechanism (e.g., `window`)
|
|
132
|
+
- `targetOrigin: string` - Target origin for postMessage
|
|
133
|
+
- `handshake?: HandshakeConfig` - Optional handshake configuration
|
|
134
|
+
|
|
135
|
+
**Returns:**
|
|
136
|
+
- `connect(): Promise<void>` - Establish connection
|
|
137
|
+
- `request(method, ...params): Promise<T>` - Make one-shot request
|
|
138
|
+
- `stream(method, ...params): AsyncIterableIterator<T>` - Make streaming request
|
|
139
|
+
- `getState(): ConnectionState` - Get current connection state
|
|
140
|
+
- `cleanup(): void` - Clean up resources
|
|
141
|
+
|
|
142
|
+
### Listener
|
|
143
|
+
|
|
144
|
+
#### `createRpcListener(config: RpcListenerConfig): RpcListener`
|
|
145
|
+
|
|
146
|
+
Creates an RPC listener for Wallet-side communication.
|
|
147
|
+
|
|
148
|
+
**Config:**
|
|
149
|
+
- `transport: RpcTransport` - The transport mechanism (e.g., `window`)
|
|
150
|
+
- `allowedOrigins: string | string[]` - Allowed origins for security
|
|
151
|
+
|
|
152
|
+
**Returns:**
|
|
153
|
+
- `handle(method, handler): void` - Register promise handler
|
|
154
|
+
- `handleStream(method, handler): void` - Register stream handler
|
|
155
|
+
- `unregister(method): void` - Unregister a handler
|
|
156
|
+
- `cleanup(): void` - Clean up resources
|
|
157
|
+
|
|
158
|
+
## Type Safety
|
|
159
|
+
|
|
160
|
+
The package provides full type safety through the RPC schema:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Method names are type-checked
|
|
164
|
+
client.request('frak_sendInteraction', ...) // ✓ Valid
|
|
165
|
+
client.request('invalid_method', ...) // ✗ Type error
|
|
166
|
+
|
|
167
|
+
// Parameters are type-checked
|
|
168
|
+
client.request('frak_sendInteraction', productId, interaction) // ✓ Valid
|
|
169
|
+
client.request('frak_sendInteraction', 'wrong-params') // ✗ Type error
|
|
170
|
+
|
|
171
|
+
// Return types are inferred
|
|
172
|
+
const result = await client.request('frak_sendInteraction', ...)
|
|
173
|
+
// result is typed as SendInteractionReturnType
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
Errors are thrown as `FrakRpcError` with standard error codes:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { RpcErrorCodes } from '@frak-labs/frame-connector'
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const result = await client.request('frak_sendInteraction', ...)
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.code === RpcErrorCodes.userRejected) {
|
|
187
|
+
console.log('User rejected the request')
|
|
188
|
+
} else if (error.code === RpcErrorCodes.invalidParams) {
|
|
189
|
+
console.error('Invalid parameters:', error.message)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
This package is part of the Frak Wallet monorepo and uses Bun as the package manager.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Install dependencies (from repo root)
|
|
200
|
+
bun install
|
|
201
|
+
|
|
202
|
+
# Type check
|
|
203
|
+
bun run typecheck
|
|
204
|
+
|
|
205
|
+
# Format
|
|
206
|
+
bun run format
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Migration Guide
|
|
210
|
+
|
|
211
|
+
For existing code using the old callback-based API, migration to the new API is straightforward:
|
|
212
|
+
|
|
213
|
+
### Before (callback-based):
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Old SDK client
|
|
217
|
+
await transport.listenerRequest(
|
|
218
|
+
{ method: 'frak_listenToWalletStatus' },
|
|
219
|
+
(status) => {
|
|
220
|
+
console.log('Status:', status)
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### After (async iterator):
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// New RPC client
|
|
229
|
+
for await (const status of client.stream('frak_listenToWalletStatus')) {
|
|
230
|
+
console.log('Status:', status)
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The underlying message format remains unchanged, ensuring 100% backward compatibility.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";let __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,r)=>{for(var t in r)__webpack_require__.o(r,t)&&!__webpack_require__.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},__webpack_require__.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{createRpcListener:()=>createRpcListener,RpcErrorCodes:()=>RpcErrorCodes,ClientNotFound:()=>ClientNotFound,InternalError:()=>InternalError,decompressJson:()=>decompressJson,MethodNotFoundError:()=>MethodNotFoundError,compressJson:()=>compressJson,Deferred:()=>Deferred,createClientCompressionMiddleware:()=>createClientCompressionMiddleware,createListenerCompressionMiddleware:()=>createListenerCompressionMiddleware,createRpcClient:()=>createRpcClient,hashAndCompressData:()=>hashAndCompressData,FrakRpcError:()=>FrakRpcError,decompressDataAndCheckHash:()=>decompressDataAndCheckHash});let RpcErrorCodes={parseError:-32700,invalidRequest:-32600,methodNotFound:-32601,invalidParams:-32602,internalError:-32603,serverError:-32e3,clientNotConnected:-32001,configError:-32002,corruptedResponse:-32003,clientAborted:-32004,walletNotConnected:-32005,serverErrorForInteractionDelegation:-32006,userRejected:-32007};class FrakRpcError extends Error{code;data;constructor(e,r,t){super(r),this.code=e,this.data=t}toJSON(){return{code:this.code,message:this.message,data:this.data}}}class MethodNotFoundError extends FrakRpcError{constructor(e,r){super(RpcErrorCodes.methodNotFound,e,{method:r})}}class InternalError extends FrakRpcError{constructor(e){super(RpcErrorCodes.internalError,e)}}class ClientNotFound extends FrakRpcError{constructor(){super(RpcErrorCodes.clientNotConnected,"Client not found")}}class Deferred{_promise;_resolve;_reject;constructor(){this._promise=new Promise((e,r)=>{this._resolve=e,this._reject=r})}get promise(){return this._promise}resolve=e=>{this._resolve?.(e)};reject=e=>{this._reject?.(e)}}function createRpcClient(e){let{emittingTransport:r,listeningTransport:t,targetOrigin:o,middleware:n=[],lifecycleHandlers:s}=e,a=new Map;async function i(e){try{"clientLifecycle"in e&&s?.clientLifecycle?await s.clientLifecycle(e,{origin:o,source:null}):"iframeLifecycle"in e&&s?.iframeLifecycle&&await s.iframeLifecycle(e,{origin:o,source:null})}catch(e){console.error("[RPC Client] Lifecycle handler error:",e)}}async function c(e){let r={origin:o,source:null};for(let t of n)t.onRequest&&await t.onRequest(e,r);return e}async function d(e,r){let t={origin:o,source:null},s=r;for(let r of n)r.onResponse&&(s=await r.onResponse(e,s,t));return s}async function l(e){var r,t;let n;try{let r=new URL(e.origin).origin.toLowerCase(),t=new URL(o).origin.toLowerCase();if(r!==t)return void console.log("Not expected origin",r,t)}catch(e){console.error("[RPC Client] Invalid origin",e);return}if("object"==typeof(r=e.data)&&r&&("clientLifecycle"in r||"iframeLifecycle"in r))return void await i(e.data);if(!("object"==typeof(t=e.data)&&t&&"id"in t&&"topic"in t&&"data"in t))return;try{let r=e.data.data,t=r instanceof Uint8Array||ArrayBuffer.isView(r);n=await d(e.data,t?{result:r}:r)}catch(e){console.error("[RPC Client] Middleware error on response:",e);return}let s=a.get(e.data.id);s&&s(n)}async function p(e){let t=e;try{t=await c(e)}catch(e){throw console.error("[RPC Client] Middleware error on request:",e),e}r.postMessage(t,o)}function u(){return`${Date.now()}-${Math.random().toString(36).substring(2,9)}`}return t.addEventListener("message",l),{request:function(e){let r=u(),t=new Deferred;return a.set(r,e=>{e.error?t.reject(new FrakRpcError(e.error.code,e.error.message,e.error.data)):t.resolve(e.result),a.delete(r)}),p({id:r,topic:e.method,data:{method:e.method,params:e.params}}).catch(e=>{a.delete(r),t.reject(e)}),t.promise},listen:function(e,r){let t=u();return a.set(t,e=>{e.error?(console.error("[RPC Client] Listener error:",e.error),a.delete(t)):r(e.result)}),p({id:t,topic:e.method,data:{method:e.method,params:e.params}}).catch(e=>{console.error("[RPC Client] Failed to send listener request:",e),a.delete(t)}),()=>{a.delete(t)}},sendLifecycle:function(e){r.postMessage(e,o)},cleanup:function(){t.removeEventListener("message",l),a.clear()}}}function createRpcListener(e){let{transport:r,allowedOrigins:t,middleware:o=[],lifecycleHandlers:n}=e,s=Array.isArray(t)?t:[t],a=new Map,i=new Map;async function c(e,r){let t=r;for(let r of o)r.onRequest&&(t=await r.onRequest(e,t));return t}async function d(e,r,t){let n=r;for(let r of o)r.onResponse&&(n=await r.onResponse(e,n,t));return n}function l(e,r,t,o,n){if(!e)return void console.error("[RPC Listener] No source to send response to");let s={id:t,topic:o,data:!n.error&&(n.result instanceof Uint8Array||ArrayBuffer.isView(n.result))?n.result:n};"postMessage"in e&&"function"==typeof e.postMessage&&e.postMessage(s,{targetOrigin:r})}function p(e,r,t,o,n){l(e,r,t,o,{error:n instanceof FrakRpcError?n.toJSON():{code:RpcErrorCodes.internalError,message:n.message}})}async function u(e,r){try{"clientLifecycle"in e&&n?.clientLifecycle?await n.clientLifecycle(e,r):"iframeLifecycle"in e&&n?.iframeLifecycle&&await n.iframeLifecycle(e,r)}catch(e){console.error("[RPC Listener] Lifecycle handler error:",e)}}async function _(e){var r,t;let o;if(!function(e){try{if(s.includes("*"))return!0;let r=new URL(e).origin.toLowerCase();return s.some(e=>{let t=new URL(e).origin.toLowerCase();return r===t})}catch(e){return console.error("[RPC Listener] Invalid origin",e),!1}}(e.origin))return void console.warn("[RPC Listener] Message from disallowed origin:",e.origin);let n={origin:e.origin,source:e.source};if("object"==typeof(r=e.data)&&r&&("clientLifecycle"in r||"iframeLifecycle"in r))return void await u(e.data,n);if("object"==typeof(t=e.data)&&t&&"id"in t&&"topic"in t&&"data"in t){try{o=await c(e.data,n)}catch(r){p(e.source,e.origin,e.data.id,e.data.topic,r instanceof Error?r:Error(String(r)));return}try{await f(e,o)}catch(r){p(e.source,e.origin,e.data.id,e.data.topic,r instanceof Error?r:Error(String(r)))}}}async function f(e,r){let{id:t,topic:o,data:n}=e.data,s=a.get(o);if(s){let a=await s(n,r),i=await d(e.data,{result:a},r);l(e.source,e.origin,t,o,i);return}let c=i.get(o);if(c){let s=async n=>{let s;try{s=await d(e.data,{result:n},r)}catch(e){console.error("[RPC Listener] Middleware failed on stream chunk:",e);return}l(e.source,e.origin,t,o,s)};await c(n,s,r);return}console.error("[RPC Listener] No handler found for method:",{topic:o,handlers:i.keys(),promiseHandler:a.keys()}),p(e.source,e.origin,t,o,new FrakRpcError(RpcErrorCodes.methodNotFound,`Method not found: ${o}`))}return r.addEventListener("message",_),{handle:function(e,r){a.set(e,r)},handleStream:function(e,r){i.set(e,r)},unregister:function(e){a.delete(e),i.delete(e)},cleanup:function(){r.removeEventListener("message",_),a.clear(),i.clear()}}}let index_js_namespaceObject=require("@jsonjoy.com/json-pack/lib/cbor/index.js"),external_viem_namespaceObject=require("viem"),encoder=new index_js_namespaceObject.CborEncoder,decoder=new index_js_namespaceObject.CborDecoder;function hashAndCompressData(e){let r={...e,validationHash:hashJson(e??{})};return encoder.encode(r)}function decompressDataAndCheckHash(e){if(!e.length)throw new FrakRpcError(RpcErrorCodes.corruptedResponse,"Missing compressed data");let r=decompressJson(e);if(!r)throw new FrakRpcError(RpcErrorCodes.corruptedResponse,"Invalid compressed data");if(!r?.validationHash)throw new FrakRpcError(RpcErrorCodes.corruptedResponse,"Missing validation hash");let{validationHash:t,...o}=r,n=hashJson(o);if(n!==r.validationHash)throw console.warn("Validation error",{validationHash:t,rawResultData:o,parsedData:r,expectedValidationHashes:n,recomputedHash:hashJson(void 0)}),new FrakRpcError(RpcErrorCodes.corruptedResponse,"Invalid validation hash");return r}function compressJson(e){return encoder.encode(e)}function decompressJson(e){try{return decoder.decode(e)}catch(r){return console.error("Invalid compressed data",{e:r,data:e}),null}}function hashJson(e){return(0,external_viem_namespaceObject.sha256)(encoder.encode(e))}let createClientCompressionMiddleware=()=>({onRequest:(e,r)=>{if(e.data instanceof Uint8Array||ArrayBuffer.isView(e.data))return r;try{e.data=hashAndCompressData(e.data)}catch(e){console.error("[Compression Middleware] Failed to compress request",e)}return r},onResponse:(e,r,t)=>{if(r.error||!(r.result instanceof Uint8Array||ArrayBuffer.isView(r.result)))return r;try{let{validationHash:e,...t}=decompressDataAndCheckHash(r.result);"object"==typeof t&&null!==t&&"result"in t?r.result=t.result:r.result=t}catch(e){console.error("[Compression Middleware] Failed to decompress response",e)}return r}}),createListenerCompressionMiddleware=()=>({onRequest:(e,r)=>{if(!(e.data instanceof Uint8Array||ArrayBuffer.isView(e.data)))return r;try{let{validationHash:r,...t}=decompressDataAndCheckHash(e.data);"object"==typeof t&&"params"in t?e.data=t.params:e.data=t}catch(e){throw console.error("[Compression Middleware] Failed to decompress request",e),e}return r},onResponse:(e,r,t)=>{if(r.error||r.result instanceof Uint8Array||ArrayBuffer.isView(r.result))return r;try{let t={method:e.topic,result:r.result};r.result=hashAndCompressData(t)}catch(e){console.error("[Compression Middleware] Failed to compress response",e)}return r}});for(var __webpack_i__ in exports.ClientNotFound=__webpack_exports__.ClientNotFound,exports.Deferred=__webpack_exports__.Deferred,exports.FrakRpcError=__webpack_exports__.FrakRpcError,exports.InternalError=__webpack_exports__.InternalError,exports.MethodNotFoundError=__webpack_exports__.MethodNotFoundError,exports.RpcErrorCodes=__webpack_exports__.RpcErrorCodes,exports.compressJson=__webpack_exports__.compressJson,exports.createClientCompressionMiddleware=__webpack_exports__.createClientCompressionMiddleware,exports.createListenerCompressionMiddleware=__webpack_exports__.createListenerCompressionMiddleware,exports.createRpcClient=__webpack_exports__.createRpcClient,exports.createRpcListener=__webpack_exports__.createRpcListener,exports.decompressDataAndCheckHash=__webpack_exports__.decompressDataAndCheckHash,exports.decompressJson=__webpack_exports__.decompressJson,exports.hashAndCompressData=__webpack_exports__.hashAndCompressData,__webpack_exports__)-1===["ClientNotFound","Deferred","FrakRpcError","InternalError","MethodNotFoundError","RpcErrorCodes","compressJson","createClientCompressionMiddleware","createListenerCompressionMiddleware","createRpcClient","createRpcListener","decompressDataAndCheckHash","decompressJson","hashAndCompressData"].indexOf(__webpack_i__)&&(exports[__webpack_i__]=__webpack_exports__[__webpack_i__]);Object.defineProperty(exports,"__esModule",{value:!0});
|