Selaa lähdekoodia

refactor(component): rename instance_id to context_key

Rename the instance_id property to context_key to better reflect its
purpose in the component context preservation system. The context_key
is used to identify and restore component context across action requests
when using the spry-context tag.

Also add comprehensive documentation for the spry-context tag including:
- Usage examples and how the context preservation flow works
- Security warnings about replay attacks with encrypted context
- Best practices for using context safely
- CSS styles for warning boxes and lifecycle diagrams
Billy Barrow 6 päivää sitten
vanhempi
sitoutus
bf67eeb230

+ 27 - 2
demo/Pages/ComponentsOverviewPage.vala

@@ -136,14 +136,39 @@ public class ComponentsOverviewPage : PageComponent {
                 <div class="warning-box">
                     <p>
                         <strong>⚠️ Critical Concept:</strong> The template DOM is fresh on every request.
-                        Any state set via setters (like <code>title</code>, <code>completed</code>) is 
+                        Any state set via setters (like <code>title</code>, <code>completed</code>) is
                         NOT available in <code>handle_action()</code>.
                     </p>
                     <p>
                         Always use <code>prepare()</code> to fetch data from stores and set up the template.
-                        Use <code>hx-vals</code> to pass data between requests.
+                        To pass data to actions, use one of these approaches:
                     </p>
                 </div>
+                
+                <div class="info-box">
+                    <h4>Option 1: spry-context (Recommended for Properties)</h4>
+                    <p>
+                        Use <code>&lt;spry-context property="name"/&gt;</code> to automatically preserve
+                        property values across requests.
+                    </p>
+                    <ul>
+                        <li>Properties are encrypted and included in action URLs automatically</li>
+                        <li>Values are restored to properties before <code>handle_action()</code> is called</li>
+                        <li>Example: <code>&lt;spry-context property="item_id"/&gt;</code></li>
+                    </ul>
+                </div>
+                
+                <div class="info-box">
+                    <h4>Option 2: hx-vals (Manual Query Parameters)</h4>
+                    <p>
+                        Use the <code>hx-vals</code> attribute to pass data as query parameters.
+                    </p>
+                    <ul>
+                        <li>Values are included in the request as query parameters</li>
+                        <li>Must be read manually via <code>http_context.request.query_params.get_any_or_default("key")</code></li>
+                        <li>Example: <code>hx-vals='{"id": "123"}'</code></li>
+                    </ul>
+                </div>
             </section>
             
             <section class="doc-section">

+ 57 - 0
demo/Pages/ComponentsTemplateSyntaxPage.vala

@@ -131,6 +131,51 @@ public class ComponentsTemplateSyntaxPage : PageComponent {
                 <spry-component name="CodeBlockComponent" sid="component-vala"/>
             </section>
             
+            <section class="doc-section">
+                <h3><code>&lt;spry-context&gt;</code> - Context Property Preservation</h3>
+                <p>
+                    The <code>&lt;spry-context&gt;</code> tag marks a property to be preserved across
+                    action requests. When a component action is triggered, properties marked with this
+                    tag are encrypted and included in the action URL, then restored when the action
+                    is handled.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="context-example"/>
+                
+                <h4>How It Works</h4>
+                <ul>
+                    <li>Properties marked with <code>&lt;spry-context property="name"/&gt;</code> are tracked</li>
+                    <li>When an action is triggered, these properties are encrypted using <code>CryptographyProvider</code></li>
+                    <li>The encrypted context is included in the action URL</li>
+                    <li>The server decrypts and restores the properties before calling <code>handle_action()</code></li>
+                </ul>
+                
+                <div class="warning-box">
+                    <h4>⚠️ Security Warning: Replay Attacks</h4>
+                    <p>
+                        The encrypted context can be captured and replayed by malicious actors. While the
+                        data cannot be tampered with (it's cryptographically signed), an attacker could
+                        capture a valid request and replay it later.
+                    </p>
+                    <ul>
+                        <li><strong>Do not</strong> use context for sensitive data that could be exploited if replayed</li>
+                        <li><strong>Do not</strong> use context for authentication or authorization decisions</li>
+                        <li><strong>Consider</strong> adding timestamps or expiration for time-sensitive operations</li>
+                    </ul>
+                    <p>
+                        <strong>Example attack:</strong> If context contains admin privileges, an attacker
+                        could capture a request from an admin session and replay it to gain unauthorized access.
+                    </p>
+                </div>
+                
+                <h4>Best Practices</h4>
+                <ul>
+                    <li>Use context for non-sensitive data like IDs, flags, or UI configuration</li>
+                    <li>Validate context data on the server before using it</li>
+                    <li>Keep context data minimal - only include what's needed</li>
+                </ul>
+            </section>
+            
             <section class="doc-section">
                 <h3><code>spry-res</code> - Static Resources</h3>
                 <p>
@@ -263,6 +308,18 @@ add_globals_from(header);  // Includes header in response for OOB swap""";
     header.title = "My App";
 }""";
         
+        // Context example
+        var context_example = get_component_child<CodeBlockComponent>("context-example");
+        context_example.language = "HTML";
+        context_example.code = """<!-- Mark properties to be preserved across actions -->
+<spry-context property="demo_component_name"/>
+<spry-context property="source_file"/>
+
+<div sid="host">
+    <span content-expr="this.demo_component_name">Demo</span>
+    <button spry-action=":ShowSource" spry-target="host">Show Source</button>
+</div>""";
+        
         // Static resources example
         var res_example = get_component_child<CodeBlockComponent>("res-example");
         res_example.language = "HTML";

+ 104 - 0
demo/Static/docs.css

@@ -561,6 +561,45 @@ blockquote code {
   background-color: rgba(0, 0, 0, 0.05);
 }
 
+/* ==========================================================================
+   Warning Boxes
+   ========================================================================== */
+
+.warning-box {
+  background: #fff3cd;
+  border: 1px solid #ffc107;
+  border-left: 4px solid #ffc107;
+  padding: 1rem;
+  margin: 1rem 0;
+  border-radius: 4px;
+}
+
+.warning-box h4 {
+  margin-top: 0;
+  margin-bottom: 0.5rem;
+  color: #856404;
+}
+
+.warning-box p {
+  margin: 0;
+  color: #856404;
+}
+
+.warning-box ul {
+  margin: 0.5rem 0 0;
+  padding-left: 1.5rem;
+  color: #856404;
+}
+
+.warning-box li {
+  margin: 0.25rem 0;
+}
+
+.warning-box code {
+  background-color: rgba(133, 100, 4, 0.1);
+  border-color: rgba(133, 100, 4, 0.2);
+}
+
 /* ==========================================================================
    Horizontal Rules
    ========================================================================== */
@@ -884,6 +923,71 @@ hr {
   white-space: pre;
 }
 
+/* ==========================================================================
+   Lifecycle Diagram
+   ========================================================================== */
+
+.lifecycle-diagram {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: var(--spacing-xl) 0;
+  padding: var(--spacing-lg);
+  background-color: var(--color-sidebar-bg);
+  border-radius: 8px;
+  border: 1px solid var(--color-border);
+}
+
+.lifecycle-step {
+  display: flex;
+  align-items: flex-start;
+  gap: var(--spacing-md);
+  width: 100%;
+  max-width: 500px;
+  padding: var(--spacing-md);
+  background-color: var(--color-background);
+  border: 1px solid var(--color-border);
+  border-radius: 6px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.step-number {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 32px;
+  height: 32px;
+  background-color: var(--color-primary);
+  color: white;
+  font-weight: 600;
+  font-size: var(--font-size-small);
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.step-content {
+  flex: 1;
+}
+
+.step-content h4 {
+  margin: 0 0 var(--spacing-xs);
+  font-size: 1rem;
+  color: var(--color-text);
+}
+
+.step-content p {
+  margin: 0;
+  font-size: var(--font-size-small);
+  color: var(--color-text-muted);
+}
+
+.lifecycle-arrow {
+  font-size: 1.5rem;
+  color: var(--color-primary);
+  padding: var(--spacing-sm) 0;
+  line-height: 1;
+}
+
 /* ==========================================================================
    Utility Classes
    ========================================================================== */

+ 1 - 1
important-details.md

@@ -169,7 +169,7 @@ Use `spry-unique` to generate unique IDs for elements that need stable targeting
 - Cannot specify an `id` attribute on the same element
 - Cannot be used inside `spry-per-*` loops (or children of loops)
 
-The generated ID format is `_spry-unique-{counter}-{instance_id}`.
+The generated ID format is `_spry-unique-{counter}-{context_key}`.
 
 ### Required Scripts for SSE
 

+ 6 - 4
src/Component.vala

@@ -19,7 +19,7 @@ namespace Spry {
         
         private static Dictionary<Type, ComponentTemplate> templates;
         private static Mutex templates_lock = Mutex();
-        public string instance_id { get; internal set; default = Uuid.string_random(); }
+        public string context_key { get; internal set; default = Uuid.string_random(); }
         
         public abstract string markup { get; }
         public virtual StatusCode get_status() {
@@ -282,7 +282,7 @@ namespace Spry {
                     var context = new ComponentContext() {
                         type_name = this.get_type().name(),
                         timestamp = new DateTime.now_utc(),
-                        instance_id = instance_id,
+                        context_key = context_key,
                         data = data
                     };
                     var context_blob = _cryptography_provider.author_component_context_blob(context);
@@ -387,7 +387,8 @@ namespace Spry {
                 node.set_attribute("sse-swap", @"_spry-dynamic-$name");
                 node.set_attribute("hx-swap", "outerHTML");
                 if(!node.has_attribute("id")) {
-                    node.set_attribute("id", @"_spry-dynamic-$name-$instance_id");
+                    node.set_attribute("id", @"_spry-dynamic-$name-$context_key");
+                    node.set_attribute("id", @"_spry-dynamic-$name-$context_key");
                 }
                 node.remove_attribute("spry-dynamic");
             }
@@ -403,7 +404,8 @@ namespace Spry {
                 if(node.get_attributes().keys.any(a => a.has_prefix("spry-per-")) || has_any_parent_where(node, n => n.get_attributes().keys.any(a => a.has_prefix("spry-per-")))) {
                     throw new ComponentError.INVALID_TEMPLATE("The spry-unique attribute is not valid on any element or child of any element with a spry-per attribute");
                 }
-                node.set_attribute("id", @"_spry-unique-$counter-$instance_id");
+                node.set_attribute("id", @"_spry-unique-$counter-$context_key");
+                node.set_attribute("id", @"_spry-unique-$counter-$context_key");
                 counter++;
             }
         }

+ 1 - 1
src/ComponentEndpoint.vala

@@ -38,7 +38,7 @@ namespace Spry {
                     return new HttpStringResult ("Context mismatch", StatusCode.BAD_REQUEST);
                 }
 
-                component.instance_id = context.instance_id;
+                component.context_key = context.context_key;
                 
                 foreach(var prop in context.data) {
                     unowned var component_class = component.get_class ();

+ 2 - 2
src/CryptographyProvider.vala

@@ -54,14 +54,14 @@ namespace Spry {
     public class ComponentContext {
 
         public string type_name { get; set; }
-        public string instance_id { get; set; }
+        public string context_key { get; set; }
         public DateTime timestamp { get; set; }
         public Properties data { get; set; }
 
         public static PropertyMapper<ComponentContext> get_mapper() {
             return PropertyMapper.build_for<ComponentContext>(cfg => {
                 cfg.map<string>("c", o => o.type_name, (o, v) => o.type_name = v);
-                cfg.map<string>("i", o => o.instance_id, (o, v) => o.instance_id = v);
+                cfg.map<string>("k", o => o.context_key, (o, v) => o.context_key = v);
                 cfg.map<string>("t", o => o.timestamp.format_iso8601(), (o, v) => o.timestamp = new DateTime.from_iso8601(v, new TimeZone.utc()));
                 cfg.map<Properties>("d", o => o.data, (o, v) => o.data = v);
                 cfg.set_constructor(() => new ComponentContext());