Spaces:
Sleeping
Sleeping
secure-ish
Browse files- app.py +432 -23
- verification_helper.js +192 -0
app.py
CHANGED
|
@@ -5,6 +5,8 @@ import uuid
|
|
| 5 |
from datetime import datetime
|
| 6 |
from typing import Dict, List, Optional, Tuple
|
| 7 |
import hashlib
|
|
|
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
# Create data directories
|
|
@@ -16,6 +18,123 @@ IMAGES_DIR = DATA_DIR / "images"
|
|
| 16 |
for dir_path in [DATA_DIR, PICLETS_DIR, COLLECTIONS_DIR, IMAGES_DIR]:
|
| 17 |
dir_path.mkdir(exist_ok=True)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
class PicletStorage:
|
| 20 |
"""Handles saving and retrieving Piclet data using file-based storage"""
|
| 21 |
|
|
@@ -107,7 +226,8 @@ class PicletStorage:
|
|
| 107 |
"primaryType": data.get("primaryType"),
|
| 108 |
"tier": data.get("tier"),
|
| 109 |
"created_at": data.get("created_at"),
|
| 110 |
-
"has_image": "image_filename" in data
|
|
|
|
| 111 |
}
|
| 112 |
piclets.append(summary)
|
| 113 |
|
|
@@ -142,7 +262,7 @@ class PicletStorage:
|
|
| 142 |
print(f"Error getting image for {piclet_id}: {e}")
|
| 143 |
return None
|
| 144 |
|
| 145 |
-
def save_piclet_api(piclet_json: str, image_file=None) -> str:
|
| 146 |
"""
|
| 147 |
API endpoint to save a Piclet
|
| 148 |
"""
|
|
@@ -159,6 +279,20 @@ def save_piclet_api(piclet_json: str, image_file=None) -> str:
|
|
| 159 |
"error": f"Missing required field: {field}"
|
| 160 |
})
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
# Save the Piclet
|
| 163 |
success, result = PicletStorage.save_piclet(piclet_data, image_file)
|
| 164 |
|
|
@@ -166,7 +300,8 @@ def save_piclet_api(piclet_json: str, image_file=None) -> str:
|
|
| 166 |
return json.dumps({
|
| 167 |
"success": True,
|
| 168 |
"piclet_id": result,
|
| 169 |
-
"message": "Piclet saved successfully"
|
|
|
|
| 170 |
})
|
| 171 |
else:
|
| 172 |
return json.dumps({
|
|
@@ -257,6 +392,87 @@ def get_piclet_image_api(piclet_id: str):
|
|
| 257 |
print(f"Error in get_piclet_image_api: {e}")
|
| 258 |
return None
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
# Create example Piclet data for testing
|
| 261 |
def create_example_piclet() -> str:
|
| 262 |
"""Create an example Piclet for testing"""
|
|
@@ -330,9 +546,86 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
|
|
| 330 |
""")
|
| 331 |
|
| 332 |
with gr.Tabs():
|
| 333 |
-
# Save
|
| 334 |
-
with gr.Tab("
|
| 335 |
-
gr.Markdown("### Save a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
with gr.Row():
|
| 338 |
with gr.Column(scale=2):
|
|
@@ -428,17 +721,75 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
|
|
| 428 |
outputs=image_output
|
| 429 |
)
|
| 430 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
# API Documentation Tab
|
| 432 |
-
with gr.Tab("📖 API Documentation"):
|
| 433 |
gr.Markdown("""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
## API Endpoints
|
| 435 |
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
-
###
|
| 439 |
-
- **Function**: `save_piclet_api(piclet_json, image_file=None)`
|
| 440 |
-
- **Input**:
|
| 441 |
- **Output**: JSON response with success status and Piclet ID
|
|
|
|
| 442 |
|
| 443 |
### 2. Get Piclet
|
| 444 |
- **Function**: `get_piclet_api(piclet_id)`
|
|
@@ -496,27 +847,62 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
|
|
| 496 |
}
|
| 497 |
```
|
| 498 |
|
| 499 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
|
|
|
| 501 |
```javascript
|
| 502 |
// Connect to this Gradio space
|
| 503 |
const client = await window.gradioClient.Client.connect("your-space-name");
|
| 504 |
|
| 505 |
-
//
|
| 506 |
-
const
|
| 507 |
-
JSON.stringify(picletData),
|
| 508 |
-
|
|
|
|
|
|
|
| 509 |
]);
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
| 515 |
|
| 516 |
-
//
|
| 517 |
-
const
|
| 518 |
-
|
|
|
|
|
|
|
| 519 |
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
```
|
| 521 |
|
| 522 |
## Storage Structure
|
|
@@ -527,6 +913,29 @@ with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app:
|
|
| 527 |
- **Collections**: `./data/collections/*.json` (future feature)
|
| 528 |
|
| 529 |
All data is stored locally in the Hugging Face Space persistent storage.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
""")
|
| 531 |
|
| 532 |
# Launch the app
|
|
|
|
| 5 |
from datetime import datetime
|
| 6 |
from typing import Dict, List, Optional, Tuple
|
| 7 |
import hashlib
|
| 8 |
+
import hmac
|
| 9 |
+
import time
|
| 10 |
from pathlib import Path
|
| 11 |
|
| 12 |
# Create data directories
|
|
|
|
| 18 |
for dir_path in [DATA_DIR, PICLETS_DIR, COLLECTIONS_DIR, IMAGES_DIR]:
|
| 19 |
dir_path.mkdir(exist_ok=True)
|
| 20 |
|
| 21 |
+
# Secret key for verification (in production, use environment variable)
|
| 22 |
+
SECRET_KEY = os.getenv("PICLET_SECRET_KEY", "piclets-dev-key-change-in-production")
|
| 23 |
+
|
| 24 |
+
class PicletVerification:
|
| 25 |
+
"""Handles Piclet authenticity verification"""
|
| 26 |
+
|
| 27 |
+
@staticmethod
|
| 28 |
+
def create_signature(piclet_data: dict, timestamp: int, generation_data: dict = None) -> str:
|
| 29 |
+
"""
|
| 30 |
+
Create a cryptographic signature for a Piclet
|
| 31 |
+
Uses HMAC-SHA256 with the secret key
|
| 32 |
+
"""
|
| 33 |
+
# Create deterministic string from core Piclet data
|
| 34 |
+
core_data = {
|
| 35 |
+
"name": piclet_data.get("name", ""),
|
| 36 |
+
"primaryType": piclet_data.get("primaryType", ""),
|
| 37 |
+
"baseStats": piclet_data.get("baseStats", {}),
|
| 38 |
+
"movepool": piclet_data.get("movepool", []),
|
| 39 |
+
"timestamp": timestamp
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Add generation metadata if provided
|
| 43 |
+
if generation_data:
|
| 44 |
+
core_data["generation_data"] = generation_data
|
| 45 |
+
|
| 46 |
+
# Create deterministic JSON string (sorted keys)
|
| 47 |
+
data_string = json.dumps(core_data, sort_keys=True, separators=(',', ':'))
|
| 48 |
+
|
| 49 |
+
# Create HMAC signature
|
| 50 |
+
signature = hmac.new(
|
| 51 |
+
SECRET_KEY.encode('utf-8'),
|
| 52 |
+
data_string.encode('utf-8'),
|
| 53 |
+
hashlib.sha256
|
| 54 |
+
).hexdigest()
|
| 55 |
+
|
| 56 |
+
return signature
|
| 57 |
+
|
| 58 |
+
@staticmethod
|
| 59 |
+
def verify_signature(piclet_data: dict, provided_signature: str, timestamp: int, generation_data: dict = None) -> bool:
|
| 60 |
+
"""
|
| 61 |
+
Verify a Piclet's signature
|
| 62 |
+
Returns True if signature is valid
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
expected_signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data)
|
| 66 |
+
return hmac.compare_digest(expected_signature, provided_signature)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"Signature verification error: {e}")
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def create_verification_data(piclet_data: dict, image_caption: str = None, concept_string: str = None) -> dict:
|
| 73 |
+
"""
|
| 74 |
+
Create complete verification data for a Piclet
|
| 75 |
+
Includes signature, timestamp, and generation metadata
|
| 76 |
+
"""
|
| 77 |
+
timestamp = int(time.time())
|
| 78 |
+
|
| 79 |
+
# Generation metadata
|
| 80 |
+
generation_data = {
|
| 81 |
+
"image_caption": image_caption or "",
|
| 82 |
+
"concept_string": concept_string or "",
|
| 83 |
+
"generation_method": "official_app"
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Create signature
|
| 87 |
+
signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data)
|
| 88 |
+
|
| 89 |
+
return {
|
| 90 |
+
"signature": signature,
|
| 91 |
+
"timestamp": timestamp,
|
| 92 |
+
"generation_data": generation_data,
|
| 93 |
+
"verification_version": "1.0"
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
@staticmethod
|
| 97 |
+
def is_verified_piclet(piclet_data: dict) -> Tuple[bool, str]:
|
| 98 |
+
"""
|
| 99 |
+
Check if a Piclet has valid verification
|
| 100 |
+
Returns: (is_verified, status_message)
|
| 101 |
+
"""
|
| 102 |
+
try:
|
| 103 |
+
# Check for verification data
|
| 104 |
+
if "verification" not in piclet_data:
|
| 105 |
+
return False, "No verification data found"
|
| 106 |
+
|
| 107 |
+
verification = piclet_data["verification"]
|
| 108 |
+
required_fields = ["signature", "timestamp", "generation_data"]
|
| 109 |
+
|
| 110 |
+
for field in required_fields:
|
| 111 |
+
if field not in verification:
|
| 112 |
+
return False, f"Missing verification field: {field}"
|
| 113 |
+
|
| 114 |
+
# Verify signature
|
| 115 |
+
is_valid = PicletVerification.verify_signature(
|
| 116 |
+
piclet_data,
|
| 117 |
+
verification["signature"],
|
| 118 |
+
verification["timestamp"],
|
| 119 |
+
verification["generation_data"]
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
if not is_valid:
|
| 123 |
+
return False, "Invalid signature - Piclet may be modified or fake"
|
| 124 |
+
|
| 125 |
+
# Check timestamp (reject if too old - 24 hours)
|
| 126 |
+
current_time = int(time.time())
|
| 127 |
+
piclet_time = verification["timestamp"]
|
| 128 |
+
|
| 129 |
+
# Allow some flexibility for testing (24 hours)
|
| 130 |
+
if current_time - piclet_time > 86400: # 24 hours
|
| 131 |
+
return False, "Piclet signature is too old (>24 hours)"
|
| 132 |
+
|
| 133 |
+
return True, "Verified authentic Piclet"
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
return False, f"Verification error: {str(e)}"
|
| 137 |
+
|
| 138 |
class PicletStorage:
|
| 139 |
"""Handles saving and retrieving Piclet data using file-based storage"""
|
| 140 |
|
|
|
|
| 226 |
"primaryType": data.get("primaryType"),
|
| 227 |
"tier": data.get("tier"),
|
| 228 |
"created_at": data.get("created_at"),
|
| 229 |
+
"has_image": "image_filename" in data,
|
| 230 |
+
"verified": data.get("verified", False)
|
| 231 |
}
|
| 232 |
piclets.append(summary)
|
| 233 |
|
|
|
|
| 262 |
print(f"Error getting image for {piclet_id}: {e}")
|
| 263 |
return None
|
| 264 |
|
| 265 |
+
def save_piclet_api(piclet_json: str, signature: str = "", image_file=None) -> str:
|
| 266 |
"""
|
| 267 |
API endpoint to save a Piclet
|
| 268 |
"""
|
|
|
|
| 279 |
"error": f"Missing required field: {field}"
|
| 280 |
})
|
| 281 |
|
| 282 |
+
# Verify Piclet authenticity
|
| 283 |
+
is_verified, verification_message = PicletVerification.is_verified_piclet(piclet_data)
|
| 284 |
+
|
| 285 |
+
if not is_verified:
|
| 286 |
+
return json.dumps({
|
| 287 |
+
"success": False,
|
| 288 |
+
"error": f"Verification failed: {verification_message}",
|
| 289 |
+
"verification_required": True
|
| 290 |
+
})
|
| 291 |
+
|
| 292 |
+
# Add verification status to stored data
|
| 293 |
+
piclet_data["verified"] = True
|
| 294 |
+
piclet_data["verification_message"] = verification_message
|
| 295 |
+
|
| 296 |
# Save the Piclet
|
| 297 |
success, result = PicletStorage.save_piclet(piclet_data, image_file)
|
| 298 |
|
|
|
|
| 300 |
return json.dumps({
|
| 301 |
"success": True,
|
| 302 |
"piclet_id": result,
|
| 303 |
+
"message": "Verified Piclet saved successfully",
|
| 304 |
+
"verified": True
|
| 305 |
})
|
| 306 |
else:
|
| 307 |
return json.dumps({
|
|
|
|
| 392 |
print(f"Error in get_piclet_image_api: {e}")
|
| 393 |
return None
|
| 394 |
|
| 395 |
+
def sign_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "") -> str:
|
| 396 |
+
"""
|
| 397 |
+
API endpoint to sign a Piclet (for static frontend integration)
|
| 398 |
+
This allows static sites to generate verified Piclets without exposing the secret key
|
| 399 |
+
"""
|
| 400 |
+
try:
|
| 401 |
+
# Parse the JSON data
|
| 402 |
+
piclet_data = json.loads(piclet_json)
|
| 403 |
+
|
| 404 |
+
# Validate required fields
|
| 405 |
+
required_fields = ["name", "primaryType", "baseStats", "movepool"]
|
| 406 |
+
for field in required_fields:
|
| 407 |
+
if field not in piclet_data:
|
| 408 |
+
return json.dumps({
|
| 409 |
+
"success": False,
|
| 410 |
+
"error": f"Missing required field: {field}"
|
| 411 |
+
})
|
| 412 |
+
|
| 413 |
+
# Create verification data using server's secret key
|
| 414 |
+
verification_data = PicletVerification.create_verification_data(
|
| 415 |
+
piclet_data,
|
| 416 |
+
image_caption=image_caption,
|
| 417 |
+
concept_string=concept_string
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
# Add verification to Piclet data
|
| 421 |
+
verified_piclet = {**piclet_data, "verification": verification_data}
|
| 422 |
+
|
| 423 |
+
return json.dumps({
|
| 424 |
+
"success": True,
|
| 425 |
+
"verified_piclet": verified_piclet,
|
| 426 |
+
"signature": verification_data["signature"],
|
| 427 |
+
"message": "Piclet signed successfully - ready for storage"
|
| 428 |
+
})
|
| 429 |
+
|
| 430 |
+
except json.JSONDecodeError:
|
| 431 |
+
return json.dumps({
|
| 432 |
+
"success": False,
|
| 433 |
+
"error": "Invalid JSON format"
|
| 434 |
+
})
|
| 435 |
+
except Exception as e:
|
| 436 |
+
return json.dumps({
|
| 437 |
+
"success": False,
|
| 438 |
+
"error": f"Signing error: {str(e)}"
|
| 439 |
+
})
|
| 440 |
+
|
| 441 |
+
def sign_and_save_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "", image_file=None) -> str:
|
| 442 |
+
"""
|
| 443 |
+
API endpoint to sign AND save a Piclet in one step (convenience method)
|
| 444 |
+
Perfect for static frontends - no secret key needed on client side
|
| 445 |
+
"""
|
| 446 |
+
try:
|
| 447 |
+
# First, sign the Piclet
|
| 448 |
+
sign_result_json = sign_piclet_api(piclet_json, image_caption, concept_string)
|
| 449 |
+
sign_result = json.loads(sign_result_json)
|
| 450 |
+
|
| 451 |
+
if not sign_result["success"]:
|
| 452 |
+
return sign_result_json
|
| 453 |
+
|
| 454 |
+
# Then save the verified Piclet
|
| 455 |
+
verified_piclet_json = json.dumps(sign_result["verified_piclet"])
|
| 456 |
+
save_result_json = save_piclet_api(verified_piclet_json, sign_result["signature"], image_file)
|
| 457 |
+
save_result = json.loads(save_result_json)
|
| 458 |
+
|
| 459 |
+
if save_result["success"]:
|
| 460 |
+
return json.dumps({
|
| 461 |
+
"success": True,
|
| 462 |
+
"piclet_id": save_result["piclet_id"],
|
| 463 |
+
"message": "Piclet signed and saved successfully",
|
| 464 |
+
"verified": True,
|
| 465 |
+
"signature": sign_result["signature"]
|
| 466 |
+
})
|
| 467 |
+
else:
|
| 468 |
+
return save_result_json
|
| 469 |
+
|
| 470 |
+
except Exception as e:
|
| 471 |
+
return json.dumps({
|
| 472 |
+
"success": False,
|
| 473 |
+
"error": f"Sign and save error: {str(e)}"
|
| 474 |
+
})
|
| 475 |
+
|
| 476 |
# Create example Piclet data for testing
|
| 477 |
def create_example_piclet() -> str:
|
| 478 |
"""Create an example Piclet for testing"""
|
|
|
|
| 546 |
""")
|
| 547 |
|
| 548 |
with gr.Tabs():
|
| 549 |
+
# Sign and Save Tab (For Static Frontends)
|
| 550 |
+
with gr.Tab("✍️ Sign & Save Piclet"):
|
| 551 |
+
gr.Markdown("### Sign and Save a Piclet (One Step - Perfect for Static Sites)")
|
| 552 |
+
gr.Markdown("🌟 **For Static Frontends**: No secret key needed! Server signs your Piclet automatically.")
|
| 553 |
+
|
| 554 |
+
with gr.Row():
|
| 555 |
+
with gr.Column(scale=2):
|
| 556 |
+
unsigned_json_input = gr.Textbox(
|
| 557 |
+
label="Unsigned Piclet JSON Data",
|
| 558 |
+
placeholder="Paste your unsigned Piclet JSON here...",
|
| 559 |
+
lines=12,
|
| 560 |
+
value=create_example_piclet()
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
with gr.Row():
|
| 564 |
+
caption_input = gr.Textbox(
|
| 565 |
+
label="Image Caption",
|
| 566 |
+
placeholder="AI-generated image description...",
|
| 567 |
+
lines=2
|
| 568 |
+
)
|
| 569 |
+
concept_input = gr.Textbox(
|
| 570 |
+
label="Concept String",
|
| 571 |
+
placeholder="Creature concept from AI...",
|
| 572 |
+
lines=2
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
with gr.Column(scale=1):
|
| 576 |
+
sign_save_image_input = gr.File(
|
| 577 |
+
label="Piclet Image (Optional)",
|
| 578 |
+
file_types=["image"]
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
sign_save_btn = gr.Button("✍️ Sign & Save Piclet", variant="primary")
|
| 582 |
+
|
| 583 |
+
sign_save_output = gr.JSON(label="Sign & Save Result")
|
| 584 |
+
|
| 585 |
+
sign_save_btn.click(
|
| 586 |
+
fn=sign_and_save_piclet_api,
|
| 587 |
+
inputs=[unsigned_json_input, caption_input, concept_input, sign_save_image_input],
|
| 588 |
+
outputs=sign_save_output
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
# Sign Only Tab
|
| 592 |
+
with gr.Tab("🔏 Sign Only"):
|
| 593 |
+
gr.Markdown("### Sign a Piclet (Two-Step Process)")
|
| 594 |
+
gr.Markdown("📝 **Advanced**: Sign first, then save separately. Useful for batch operations.")
|
| 595 |
+
|
| 596 |
+
with gr.Row():
|
| 597 |
+
with gr.Column(scale=2):
|
| 598 |
+
sign_json_input = gr.Textbox(
|
| 599 |
+
label="Unsigned Piclet JSON Data",
|
| 600 |
+
placeholder="Paste your unsigned Piclet JSON here...",
|
| 601 |
+
lines=10,
|
| 602 |
+
value=create_example_piclet()
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
with gr.Row():
|
| 606 |
+
sign_caption_input = gr.Textbox(
|
| 607 |
+
label="Image Caption",
|
| 608 |
+
placeholder="AI-generated image description..."
|
| 609 |
+
)
|
| 610 |
+
sign_concept_input = gr.Textbox(
|
| 611 |
+
label="Concept String",
|
| 612 |
+
placeholder="Creature concept from AI..."
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
with gr.Column(scale=2):
|
| 616 |
+
sign_btn = gr.Button("🔏 Sign Piclet", variant="secondary")
|
| 617 |
+
sign_output = gr.JSON(label="Signing Result")
|
| 618 |
+
|
| 619 |
+
sign_btn.click(
|
| 620 |
+
fn=sign_piclet_api,
|
| 621 |
+
inputs=[sign_json_input, sign_caption_input, sign_concept_input],
|
| 622 |
+
outputs=sign_output
|
| 623 |
+
)
|
| 624 |
+
|
| 625 |
+
# Save Piclet Tab (For Pre-Signed)
|
| 626 |
+
with gr.Tab("💾 Save Pre-Signed"):
|
| 627 |
+
gr.Markdown("### Save a Pre-Signed Piclet")
|
| 628 |
+
gr.Markdown("⚠️ **For Advanced Users**: Save a Piclet that was already signed elsewhere.")
|
| 629 |
|
| 630 |
with gr.Row():
|
| 631 |
with gr.Column(scale=2):
|
|
|
|
| 721 |
outputs=image_output
|
| 722 |
)
|
| 723 |
|
| 724 |
+
# Generate Verified Example Tab
|
| 725 |
+
with gr.Tab("✅ Generate Verified Example"):
|
| 726 |
+
gr.Markdown("""
|
| 727 |
+
### Generate a Verified Example Piclet
|
| 728 |
+
|
| 729 |
+
This creates a properly signed example Piclet that will pass verification.
|
| 730 |
+
Use this to test the verification system or as a template.
|
| 731 |
+
""")
|
| 732 |
+
|
| 733 |
+
generate_btn = gr.Button("✅ Generate Verified Example", variant="primary")
|
| 734 |
+
verified_output = gr.Textbox(
|
| 735 |
+
label="Verified Piclet JSON",
|
| 736 |
+
lines=20,
|
| 737 |
+
show_copy_button=True
|
| 738 |
+
)
|
| 739 |
+
|
| 740 |
+
generate_btn.click(
|
| 741 |
+
fn=create_verified_example_api,
|
| 742 |
+
outputs=verified_output
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
# API Documentation Tab
|
| 746 |
+
with gr.Tab("📖 API Documentation & Verification"):
|
| 747 |
gr.Markdown("""
|
| 748 |
+
## 🔐 Verification System
|
| 749 |
+
|
| 750 |
+
**All Piclets must be verified to prevent fake or modified creatures.**
|
| 751 |
+
|
| 752 |
+
### How Verification Works:
|
| 753 |
+
1. **HMAC-SHA256 Signature**: Each Piclet is signed with a secret key
|
| 754 |
+
2. **Timestamp Validation**: Signatures expire after 24 hours
|
| 755 |
+
3. **Generation Metadata**: Includes image caption and concept string
|
| 756 |
+
4. **Tamper Detection**: Any modification invalidates the signature
|
| 757 |
+
|
| 758 |
+
### Required Verification Format:
|
| 759 |
+
```json
|
| 760 |
+
{
|
| 761 |
+
"verification": {
|
| 762 |
+
"signature": "abc123...",
|
| 763 |
+
"timestamp": 1690123456,
|
| 764 |
+
"generation_data": {
|
| 765 |
+
"image_caption": "AI-generated description",
|
| 766 |
+
"concept_string": "Creature concept",
|
| 767 |
+
"generation_method": "official_app"
|
| 768 |
+
},
|
| 769 |
+
"verification_version": "1.0"
|
| 770 |
+
}
|
| 771 |
+
}
|
| 772 |
+
```
|
| 773 |
+
|
| 774 |
## API Endpoints
|
| 775 |
|
| 776 |
+
### 1. Sign & Save Piclet (Recommended for Static Sites)
|
| 777 |
+
- **Function**: `sign_and_save_piclet_api(piclet_json, image_caption, concept_string, image_file=None)`
|
| 778 |
+
- **Input**: Unsigned Piclet JSON, image caption, concept string, optional image file
|
| 779 |
+
- **Output**: JSON response with success status and Piclet ID
|
| 780 |
+
- **✅ Perfect for**: Static sites (no secret key needed)
|
| 781 |
+
|
| 782 |
+
### 2. Sign Only
|
| 783 |
+
- **Function**: `sign_piclet_api(piclet_json, image_caption, concept_string)`
|
| 784 |
+
- **Input**: Unsigned Piclet JSON, image caption, concept string
|
| 785 |
+
- **Output**: JSON response with verified Piclet and signature
|
| 786 |
+
- **Use case**: Two-step process, batch operations
|
| 787 |
|
| 788 |
+
### 3. Save Pre-Signed Piclet
|
| 789 |
+
- **Function**: `save_piclet_api(piclet_json, signature, image_file=None)`
|
| 790 |
+
- **Input**: Signed Piclet JSON, signature string, optional image file
|
| 791 |
- **Output**: JSON response with success status and Piclet ID
|
| 792 |
+
- **⚠️ Requires**: Valid verification signature
|
| 793 |
|
| 794 |
### 2. Get Piclet
|
| 795 |
- **Function**: `get_piclet_api(piclet_id)`
|
|
|
|
| 847 |
}
|
| 848 |
```
|
| 849 |
|
| 850 |
+
## Frontend Integration (JavaScript)
|
| 851 |
+
|
| 852 |
+
### Include Verification Helper
|
| 853 |
+
```html
|
| 854 |
+
<!-- Include crypto-js for HMAC generation -->
|
| 855 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
| 856 |
+
<script src="verification_helper.js"></script>
|
| 857 |
+
```
|
| 858 |
|
| 859 |
+
### Static Site Usage (Recommended)
|
| 860 |
```javascript
|
| 861 |
// Connect to this Gradio space
|
| 862 |
const client = await window.gradioClient.Client.connect("your-space-name");
|
| 863 |
|
| 864 |
+
// Sign and save in one step (NO SECRET KEY NEEDED!)
|
| 865 |
+
const result = await client.predict("/sign_and_save_piclet_api", [
|
| 866 |
+
JSON.stringify(picletData), // Unsigned Piclet data
|
| 867 |
+
imageCaption, // AI-generated caption
|
| 868 |
+
conceptString, // Creature concept
|
| 869 |
+
imageFile // Optional image
|
| 870 |
]);
|
| 871 |
|
| 872 |
+
const response = JSON.parse(result.data[0]);
|
| 873 |
+
if (response.success) {
|
| 874 |
+
console.log(`Piclet saved with ID: ${response.piclet_id}`);
|
| 875 |
+
} else {
|
| 876 |
+
console.error(`Save failed: ${response.error}`);
|
| 877 |
+
}
|
| 878 |
|
| 879 |
+
// Two-step process (sign first, then save)
|
| 880 |
+
const signResult = await client.predict("/sign_piclet_api", [
|
| 881 |
+
JSON.stringify(picletData),
|
| 882 |
+
imageCaption,
|
| 883 |
+
conceptString
|
| 884 |
]);
|
| 885 |
+
|
| 886 |
+
const signResponse = JSON.parse(signResult.data[0]);
|
| 887 |
+
if (signResponse.success) {
|
| 888 |
+
const saveResult = await client.predict("/save_piclet_api", [
|
| 889 |
+
JSON.stringify(signResponse.verified_piclet),
|
| 890 |
+
signResponse.signature,
|
| 891 |
+
imageFile
|
| 892 |
+
]);
|
| 893 |
+
}
|
| 894 |
+
```
|
| 895 |
+
|
| 896 |
+
### Secret Key Management (Server Only)
|
| 897 |
+
```bash
|
| 898 |
+
# Set environment variable on server (HuggingFace Spaces)
|
| 899 |
+
PICLET_SECRET_KEY=your-super-secret-64-char-hex-key
|
| 900 |
+
|
| 901 |
+
# Generate secure key:
|
| 902 |
+
openssl rand -hex 32
|
| 903 |
+
|
| 904 |
+
# ✅ Frontend doesn't need the secret key anymore!
|
| 905 |
+
# Server handles all signing securely
|
| 906 |
```
|
| 907 |
|
| 908 |
## Storage Structure
|
|
|
|
| 913 |
- **Collections**: `./data/collections/*.json` (future feature)
|
| 914 |
|
| 915 |
All data is stored locally in the Hugging Face Space persistent storage.
|
| 916 |
+
|
| 917 |
+
## Security Features
|
| 918 |
+
|
| 919 |
+
### ✅ Verified Piclets
|
| 920 |
+
- Generated through official app only
|
| 921 |
+
- Cryptographically signed with HMAC-SHA256
|
| 922 |
+
- Include generation metadata (image caption, concept)
|
| 923 |
+
- Tamper-proof (any modification breaks signature)
|
| 924 |
+
|
| 925 |
+
### ❌ Rejected Piclets
|
| 926 |
+
- Missing verification data
|
| 927 |
+
- Invalid or expired signatures
|
| 928 |
+
- Modified after generation
|
| 929 |
+
- Created outside official app
|
| 930 |
+
|
| 931 |
+
### Environment Variables
|
| 932 |
+
- `PICLET_SECRET_KEY`: Secret key for verification (change in production)
|
| 933 |
+
|
| 934 |
+
### Files Included
|
| 935 |
+
- `app.py`: Main server application
|
| 936 |
+
- `verification_helper.js`: Frontend helper functions
|
| 937 |
+
- `requirements.txt`: Python dependencies
|
| 938 |
+
- `API_DOCUMENTATION.md`: Complete documentation
|
| 939 |
""")
|
| 940 |
|
| 941 |
# Launch the app
|
verification_helper.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Piclet Verification Helper for Frontend
|
| 3 |
+
* Use this in your Svelte game to generate verified Piclets
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// This should match the server's secret key
|
| 7 |
+
const PICLET_SECRET_KEY = "piclets-dev-key-change-in-production";
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Create HMAC-SHA256 signature (requires crypto-js or similar)
|
| 11 |
+
* For browser compatibility, you'll need to include crypto-js:
|
| 12 |
+
* npm install crypto-js
|
| 13 |
+
* or include via CDN: https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
|
| 14 |
+
*/
|
| 15 |
+
async function createHMAC(message, key) {
|
| 16 |
+
// Browser-native crypto API version (modern browsers)
|
| 17 |
+
if (window.crypto && window.crypto.subtle) {
|
| 18 |
+
const encoder = new TextEncoder();
|
| 19 |
+
const keyData = encoder.encode(key);
|
| 20 |
+
const messageData = encoder.encode(message);
|
| 21 |
+
|
| 22 |
+
const cryptoKey = await window.crypto.subtle.importKey(
|
| 23 |
+
'raw',
|
| 24 |
+
keyData,
|
| 25 |
+
{ name: 'HMAC', hash: 'SHA-256' },
|
| 26 |
+
false,
|
| 27 |
+
['sign']
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
| 31 |
+
const hashArray = Array.from(new Uint8Array(signature));
|
| 32 |
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Fallback: Use crypto-js if available
|
| 36 |
+
if (window.CryptoJS) {
|
| 37 |
+
return CryptoJS.HmacSHA256(message, key).toString();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
throw new Error("No crypto implementation available. Include crypto-js or use modern browser.");
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Create verification data for a Piclet
|
| 45 |
+
* Call this when generating a Piclet in your game
|
| 46 |
+
*/
|
| 47 |
+
async function createPicletVerification(picletData, imageCaption = "", conceptString = "") {
|
| 48 |
+
const timestamp = Math.floor(Date.now() / 1000);
|
| 49 |
+
|
| 50 |
+
// Core data used for signature
|
| 51 |
+
const coreData = {
|
| 52 |
+
name: picletData.name || "",
|
| 53 |
+
primaryType: picletData.primaryType || "",
|
| 54 |
+
baseStats: picletData.baseStats || {},
|
| 55 |
+
movepool: picletData.movepool || [],
|
| 56 |
+
timestamp: timestamp
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
// Generation metadata
|
| 60 |
+
const generationData = {
|
| 61 |
+
image_caption: imageCaption,
|
| 62 |
+
concept_string: conceptString,
|
| 63 |
+
generation_method: "official_app"
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
// Add generation data to core data
|
| 67 |
+
coreData.generation_data = generationData;
|
| 68 |
+
|
| 69 |
+
// Create deterministic JSON string
|
| 70 |
+
const dataString = JSON.stringify(coreData, null, 0);
|
| 71 |
+
|
| 72 |
+
// Create signature
|
| 73 |
+
const signature = await createHMAC(dataString, PICLET_SECRET_KEY);
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
signature: signature,
|
| 77 |
+
timestamp: timestamp,
|
| 78 |
+
generation_data: generationData,
|
| 79 |
+
verification_version: "1.0"
|
| 80 |
+
};
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Add verification to a Piclet before saving
|
| 85 |
+
* Use this in your PicletGenerator component
|
| 86 |
+
*/
|
| 87 |
+
async function verifyAndPreparePiclet(picletData, imageCaption = "", conceptString = "") {
|
| 88 |
+
try {
|
| 89 |
+
// Create verification data
|
| 90 |
+
const verification = await createPicletVerification(picletData, imageCaption, conceptString);
|
| 91 |
+
|
| 92 |
+
// Add verification to Piclet data
|
| 93 |
+
const verifiedPiclet = {
|
| 94 |
+
...picletData,
|
| 95 |
+
verification: verification
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
return {
|
| 99 |
+
success: true,
|
| 100 |
+
piclet: verifiedPiclet,
|
| 101 |
+
signature: verification.signature
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
} catch (error) {
|
| 105 |
+
return {
|
| 106 |
+
success: false,
|
| 107 |
+
error: error.message
|
| 108 |
+
};
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Save a verified Piclet to the server
|
| 114 |
+
* Use this instead of direct API calls
|
| 115 |
+
*/
|
| 116 |
+
async function saveVerifiedPiclet(gradioClient, picletData, imageFile = null, imageCaption = "", conceptString = "") {
|
| 117 |
+
try {
|
| 118 |
+
// Create verified Piclet
|
| 119 |
+
const verificationResult = await verifyAndPreparePiclet(picletData, imageCaption, conceptString);
|
| 120 |
+
|
| 121 |
+
if (!verificationResult.success) {
|
| 122 |
+
return {
|
| 123 |
+
success: false,
|
| 124 |
+
error: `Verification failed: ${verificationResult.error}`
|
| 125 |
+
};
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Save to server
|
| 129 |
+
const result = await gradioClient.predict("/save_piclet_api", [
|
| 130 |
+
JSON.stringify(verificationResult.piclet),
|
| 131 |
+
verificationResult.signature,
|
| 132 |
+
imageFile
|
| 133 |
+
]);
|
| 134 |
+
|
| 135 |
+
return JSON.parse(result.data[0]);
|
| 136 |
+
|
| 137 |
+
} catch (error) {
|
| 138 |
+
return {
|
| 139 |
+
success: false,
|
| 140 |
+
error: `Save failed: ${error.message}`
|
| 141 |
+
};
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Example usage in your Svelte component:
|
| 147 |
+
*
|
| 148 |
+
* // In PicletGenerator.svelte or similar component
|
| 149 |
+
* import { saveVerifiedPiclet } from './verification_helper.js';
|
| 150 |
+
*
|
| 151 |
+
* async function savePiclet() {
|
| 152 |
+
* const picletData = {
|
| 153 |
+
* name: generatedName,
|
| 154 |
+
* primaryType: detectedType,
|
| 155 |
+
* baseStats: calculatedStats,
|
| 156 |
+
* movepool: generatedMoves,
|
| 157 |
+
* // ... other data
|
| 158 |
+
* };
|
| 159 |
+
*
|
| 160 |
+
* const result = await saveVerifiedPiclet(
|
| 161 |
+
* gradioClient,
|
| 162 |
+
* picletData,
|
| 163 |
+
* uploadedImageFile,
|
| 164 |
+
* imageCaption,
|
| 165 |
+
* conceptString
|
| 166 |
+
* );
|
| 167 |
+
*
|
| 168 |
+
* if (result.success) {
|
| 169 |
+
* console.log(`Piclet saved with ID: ${result.piclet_id}`);
|
| 170 |
+
* } else {
|
| 171 |
+
* console.error(`Save failed: ${result.error}`);
|
| 172 |
+
* }
|
| 173 |
+
* }
|
| 174 |
+
*/
|
| 175 |
+
|
| 176 |
+
// Export for ES modules
|
| 177 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 178 |
+
module.exports = {
|
| 179 |
+
createPicletVerification,
|
| 180 |
+
verifyAndPreparePiclet,
|
| 181 |
+
saveVerifiedPiclet
|
| 182 |
+
};
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Global functions for script tag usage
|
| 186 |
+
if (typeof window !== 'undefined') {
|
| 187 |
+
window.PicletVerification = {
|
| 188 |
+
createPicletVerification,
|
| 189 |
+
verifyAndPreparePiclet,
|
| 190 |
+
saveVerifiedPiclet
|
| 191 |
+
};
|
| 192 |
+
}
|