Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PUB-1135] Add LiveObjects examples (LiveMap and LiveCounter) #2437

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

VeskeR
Copy link
Contributor

@VeskeR VeskeR commented Feb 21, 2025

Description

Adds two code examples for the new LiveObjects feature of pub/sub SDK; for LiveMap and LiveCounter. It contains a TypeScript example showcasing a simple voting application powered by a LiveCounter, and a task list powered by a LiveMap.

Review

PR is currently placed in draft as there are still a couple of things to sort out before we can merge it into main:

The examples themselves are ready for review in terms of structure, documentation, and API usage.

How to get the sandobx API key for the app with enableChannelState flag

The easiest way is probably to clone the ably-js repo locally and checkout the integration/liveobjects branch (https://github.com/ably/ably-js/tree/integration/liveobjects). Then launch tests with the ABLY_LOG_LEVEL environment set to 4: ABLY_LOG_LEVEL=4 npm run test. Wait a couple of seconds until the app is created and one of the tests creates a new Client with an API key (10-15 seconds should be enough). Look for Ably: BaseClient(): initialized with clientOptions log in the console then, it will have a JSON output of options used to create a client. Copy the key value from those options - this is the API key you can use to run the examples in this repo.

How to launch

You may get Integrity check failed for "ably" error when doing yarn install due to ably being installed from the locally provided tgz file.
I was able to fix it as follows:
In the corresponding example app folder (e.g. ./examples/liveobjects-live-counter/javascript) run:

  • delete node_modules
  • yarn cache clean ably
  • yarn --update-checksums
  • yarn install
  • now yarn run dev should launch it successfully

Summary by CodeRabbit

  • New Features
    • Launched two interactive demos: one for real-time vote counting and another for live task management.
  • Documentation
    • Added comprehensive guides detailing how to set up, run, and test both real-time applications.
  • Chores
    • Updated project configurations and dependency management to enhance development and build processes.

Copy link

coderabbitai bot commented Feb 21, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This pull request introduces two new examples—LiveCounter and LiveMap—that utilize Ably’s real-time messaging capabilities. Both examples have configuration enhancements such as new environment variables for Ably, standardized Git ignore files, TypeScript configuration, and Vite/Tailwind CSS integrations. New HTML, script, and documentation files have been added to provide interactive, real-time functionality for voting counters and task management across multiple clients.

Changes

File(s) Change Summary
.../liveobjects-live-counter/javascript/.env.example
.../liveobjects-live-map/javascript/.env.example
Added new environment variable VITE_PUBLIC_ABLY_KEY for Ably configuration.
.../liveobjects-live-counter/javascript/.gitignore
.../liveobjects-live-map/javascript/.gitignore
Added .gitignore files ignoring common dependencies, build artifacts, and temporary files.
.../liveobjects-live-counter/javascript/README.md
.../liveobjects-live-map/javascript/README.md
Added README files with setup and usage instructions for the LiveCounter and LiveMap projects.
.../liveobjects-live-counter/javascript/index.html
.../liveobjects-live-map/javascript/index.html
Added HTML files with UI structures for the voting application (LiveCounter) and the realtime task board (LiveMap).
.../liveobjects-live-counter/javascript/package.json
.../liveobjects-live-map/javascript/package.json
Added package.json files specifying project metadata, dependencies, and scripts for both projects.
.../liveobjects-live-counter/javascript/src/ably.config.d.ts
.../liveobjects-live-map/javascript/src/ably.config.d.ts
Added TypeScript declaration files defining the global interface for live objects (counters and tasks) for improved type safety.
.../liveobjects-live-counter/javascript/src/script.ts
.../liveobjects-live-map/javascript/src/script.ts
Added script files that implement interactive features using Ably Realtime (a live voting counter and a live task management system).
.../liveobjects-live-counter/javascript/src/styles.css
.../liveobjects-live-map/javascript/src/styles.css
Added Tailwind CSS directives in the stylesheet for both projects.
.../liveobjects-live-counter/javascript/tailwind.config.js
.../liveobjects-live-map/javascript/tailwind.config.js
Added Tailwind CSS configuration files specifying content paths and theme settings.
.../liveobjects-live-counter/javascript/tsconfig.json
.../liveobjects-live-map/javascript/tsconfig.json
Added TypeScript configuration files with settings for strict type checking and output directories.
.../liveobjects-live-counter/javascript/vite-env.d.ts
.../liveobjects-live-map/javascript/vite-env.d.ts
Added Vite environment type definitions to enforce type safety for environment variables.
.../liveobjects-live-counter/javascript/vite.config.ts
.../liveobjects-live-map/javascript/vite.config.ts
Added Vite configuration files with PostCSS plugin integration (Tailwind CSS and Autoprefixer) for the build process.
.../liveobjects-live-counter/page.md
.../liveobjects-live-map/page.md
Added markdown documentation for the LiveCounter and LiveMap features, detailing setup, APIs, and usage guidelines.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant UI as Browser UI
    participant LC as LiveCounter App
    participant A as Ably Service

    U->>UI: Clicks vote button
    UI->>LC: Triggers counter update
    LC->>A: Publishes counter increment
    A-->>LC: Broadcasts updated counter
    LC->>UI: Updates counter display
Loading
sequenceDiagram
    participant U as User
    participant UI as Task Board UI
    participant LM as LiveMap App
    participant A as Ably Service

    U->>UI: Enters new task
    UI->>LM: Sends add task event
    LM->>A: Updates LiveMap with new task data
    A-->>LM: Broadcasts task update
    LM->>UI: Refreshes task list display
Loading

Poem

Hoppin' through the code garden so bright,
I found new keys to guide our real-time flight.
With live counters and maps that gleam,
Our repo’s a wonder—like a rabbit's dream!
May each update bring a joyful hop,
And every build be a leap non-stop!
🥕🐇 Happy coding under moonlit hops!


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ably-ci ably-ci temporarily deployed to ably-docs-pub-1135-live-xtvnel February 21, 2025 08:42 Inactive
@VeskeR VeskeR marked this pull request as draft February 21, 2025 08:42
@VeskeR
Copy link
Contributor Author

VeskeR commented Feb 21, 2025

@coderabbitai review

Copy link

coderabbitai bot commented Feb 21, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🔭 Outside diff range comments (1)
examples/liveobjects-live-map/javascript/src/script.ts (1)

112-132: 🛠️ Refactor suggestion

Add input validation for task title.

The task addition should include proper input validation to ensure data quality.

 function addEventListenersToButtons(root: LiveMap<DefaultRoot>) {
   addTaskButton.addEventListener('click', async () => {
     const taskTitle = taskInput.value.trim();
-    if (!taskTitle) {
+    if (!taskTitle || taskTitle.length < 3) {
+      alert('Task title must be at least 3 characters long');
       return;
     }

     const taskId = nanoid();
     taskInput.value = '';
-    await root.get('tasks').set(taskId, taskTitle);
+    try {
+      await root.get('tasks').set(taskId, taskTitle);
+    } catch (error) {
+      console.error('Failed to add task:', error);
+      alert('Failed to add task. Please try again.');
+    }
   });
🧹 Nitpick comments (8)
examples/liveobjects-live-counter/javascript/src/ably.config.d.ts (1)

4-12: Consider a more maintainable type definition.

While the current implementation works, consider making it more maintainable by using a generic type instead of hardcoding enum values:

 declare global {
   export interface LiveObjectsTypes {
     root: {
-      [Color.red]: LiveCounter;
-      [Color.green]: LiveCounter;
-      [Color.blue]: LiveCounter;
+      [K in Color]: LiveCounter;
     };
   }
 }

This approach:

  • Reduces duplication
  • Automatically updates when new colors are added to the enum
  • Maintains type safety
examples/liveobjects-live-counter/javascript/index.html (2)

15-39: Enhance accessibility and user experience.

The voting interface could benefit from improved accessibility and loading states.

 <div id="vote-options">
+  <div role="region" aria-live="polite" id="vote-status" class="sr-only"></div>
   <div class="flex justify-between items-center p-2 border-b space-x-4">
     <span class="text-red-500 font-semibold flex-grow">Red</span>
     <span class="font-bold text-gray-700" id="count-red">0</span>
-    <button class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded vote-button" data-color="red">
+    <button class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded vote-button" 
+            data-color="red" 
+            aria-label="Vote for red"
+            disabled>
+      <span class="vote-text">Vote</span>
+      <span class="loading-text hidden">Voting...</span>
     </button>
   </div>
   <!-- Similar changes for green and blue buttons -->
 </div>

Add corresponding JavaScript to handle loading states:

function handleVoteClick(button: HTMLButtonElement) {
  button.disabled = true;
  button.querySelector('.vote-text')?.classList.add('hidden');
  button.querySelector('.loading-text')?.classList.remove('hidden');
  
  // After vote completes:
  button.disabled = false;
  button.querySelector('.vote-text')?.classList.remove('hidden');
  button.querySelector('.loading-text')?.classList.add('hidden');
}

41-41: Add confirmation for reset action.

The reset button should have a confirmation step to prevent accidental resets.

-<button class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded" id="reset">Reset</button>
+<button 
+  class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded" 
+  id="reset"
+  aria-label="Reset all votes"
+  onclick="return confirm('Are you sure you want to reset all votes?')">
+  Reset
+</button>
examples/liveobjects-live-counter/page.md (1)

70-70: Minor Style Improvement on CodeSandbox Instructions
The sentence in the CodeSandbox instructions that says “update the value of your VITE_PUBLIC_ABLY_KEY variable to use your Ably API key” contains redundant wording as flagged by the static analysis tool. A small rephrase could improve readability.

🧰 Tools
🪛 LanguageTool

[style] ~70-~70: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...E_PUBLIC_ABLY_KEY` variable to use your Ably API key.

(ADVERB_REPETITION_PREMIUM)

examples/liveobjects-live-map/page.md (2)

1-4: Clarify the Opening Description
The opening lines for LiveMap provide essential context, but the phrasing “update and synchronize key/value data across in an application in realtime” could be improved for clarity. Consider rephrasing to something like:

“Enable clients to update and synchronize key/value pairs in real time throughout an application.”


70-70: Minor Style Suggestion on CodeSandbox Instructions
The sentence in the CodeSandbox instructions regarding updating the environment variable might benefit from a slight rephrasing to avoid repetition as highlighted by the static analysis tool.

🧰 Tools
🪛 LanguageTool

[style] ~70-~70: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...E_PUBLIC_ABLY_KEY` variable to use your Ably API key.

(ADVERB_REPETITION_PREMIUM)

examples/liveobjects-live-map/javascript/index.html (2)

6-7: Optimize font loading performance.

Consider adding font-display: swap to the font loading strategy to improve perceived performance.

-    <link href="https://fonts.googleapis.com/css?family=Inter" rel="stylesheet" />
+    <link href="https://fonts.googleapis.com/css?family=Inter&display=swap" rel="stylesheet" />

31-33: Add loading and empty states.

The tasks container should include loading and empty states to improve user experience.

         <div class="h-full border rounded-lg overflow-y-auto bg-white shadow-lg">
-          <div id="tasks" class="p-4 space-y-4"></div>
+          <div id="tasks" class="p-4 space-y-4">
+            <div id="loading-state" class="text-center py-4 text-gray-500">Loading tasks...</div>
+            <div id="empty-state" class="text-center py-4 text-gray-500 hidden">No tasks yet. Add your first task above!</div>
+          </div>
         </div>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08064b7 and 25be0fb.

⛔ Files ignored due to path filters (2)
  • examples/liveobjects-live-counter/javascript/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • examples/liveobjects-live-map/javascript/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (26)
  • examples/liveobjects-live-counter/javascript/.env.example (1 hunks)
  • examples/liveobjects-live-counter/javascript/.gitignore (1 hunks)
  • examples/liveobjects-live-counter/javascript/README.md (1 hunks)
  • examples/liveobjects-live-counter/javascript/index.html (1 hunks)
  • examples/liveobjects-live-counter/javascript/package.json (1 hunks)
  • examples/liveobjects-live-counter/javascript/src/ably.config.d.ts (1 hunks)
  • examples/liveobjects-live-counter/javascript/src/script.ts (1 hunks)
  • examples/liveobjects-live-counter/javascript/src/styles.css (1 hunks)
  • examples/liveobjects-live-counter/javascript/tailwind.config.js (1 hunks)
  • examples/liveobjects-live-counter/javascript/tsconfig.json (1 hunks)
  • examples/liveobjects-live-counter/javascript/vite-env.d.ts (1 hunks)
  • examples/liveobjects-live-counter/javascript/vite.config.ts (1 hunks)
  • examples/liveobjects-live-counter/page.md (1 hunks)
  • examples/liveobjects-live-map/javascript/.env.example (1 hunks)
  • examples/liveobjects-live-map/javascript/.gitignore (1 hunks)
  • examples/liveobjects-live-map/javascript/README.md (1 hunks)
  • examples/liveobjects-live-map/javascript/index.html (1 hunks)
  • examples/liveobjects-live-map/javascript/package.json (1 hunks)
  • examples/liveobjects-live-map/javascript/src/ably.config.d.ts (1 hunks)
  • examples/liveobjects-live-map/javascript/src/script.ts (1 hunks)
  • examples/liveobjects-live-map/javascript/src/styles.css (1 hunks)
  • examples/liveobjects-live-map/javascript/tailwind.config.js (1 hunks)
  • examples/liveobjects-live-map/javascript/tsconfig.json (1 hunks)
  • examples/liveobjects-live-map/javascript/vite-env.d.ts (1 hunks)
  • examples/liveobjects-live-map/javascript/vite.config.ts (1 hunks)
  • examples/liveobjects-live-map/page.md (1 hunks)
✅ Files skipped from review due to trivial changes (16)
  • examples/liveobjects-live-map/javascript/tailwind.config.js
  • examples/liveobjects-live-map/javascript/src/styles.css
  • examples/liveobjects-live-counter/javascript/src/styles.css
  • examples/liveobjects-live-map/javascript/.env.example
  • examples/liveobjects-live-counter/javascript/.env.example
  • examples/liveobjects-live-map/javascript/vite.config.ts
  • examples/liveobjects-live-counter/javascript/tailwind.config.js
  • examples/liveobjects-live-counter/javascript/README.md
  • examples/liveobjects-live-counter/javascript/vite.config.ts
  • examples/liveobjects-live-map/javascript/.gitignore
  • examples/liveobjects-live-counter/javascript/.gitignore
  • examples/liveobjects-live-map/javascript/tsconfig.json
  • examples/liveobjects-live-counter/javascript/package.json
  • examples/liveobjects-live-counter/javascript/tsconfig.json
  • examples/liveobjects-live-map/javascript/README.md
  • examples/liveobjects-live-map/javascript/package.json
🧰 Additional context used
🪛 LanguageTool
examples/liveobjects-live-counter/page.md

[style] ~70-~70: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...E_PUBLIC_ABLY_KEY` variable to use your Ably API key.

(ADVERB_REPETITION_PREMIUM)

examples/liveobjects-live-map/page.md

[style] ~70-~70: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...E_PUBLIC_ABLY_KEY` variable to use your Ably API key.

(ADVERB_REPETITION_PREMIUM)

🔇 Additional comments (12)
examples/liveobjects-live-counter/javascript/vite-env.d.ts (2)

6-8: LGTM! Well-structured type definitions.

The implementation correctly follows Vite's TypeScript configuration pattern and ensures type safety through proper use of readonly modifiers.


1-4:

❓ Verification inconclusive

Verify the security implications of using VITE_PUBLIC_ABLY_KEY.

The "PUBLIC" prefix suggests this key will be exposed to the client-side code. Please ensure this is a publish-only API key with appropriate capability restrictions to prevent unauthorized access.


🌐 Web query:

What are the security best practices for Ably API keys in client-side applications?

💡 Result:

When securing Ably API keys in client-side applications, Token Authentication is the primary recommended approach to avoid exposing sensitive credentials. Here are the key best practices:

Use Token Authentication Instead of Basic Authentication

  • Avoid Basic Authentication (direct use of API keys) in client-side code[4][6]. Basic authentication exposes your full API key string, risking compromise if intercepted.
  • Generate short-lived tokens server-side using Ably's Token Request mechanism[4][6][9]. These tokens:
    • Expire after a set time (default 60 minutes)
    • Can be scope-limited to specific channels/operations
    • Are cryptographically signed to prevent tampering
    • Don't expose your master API key secrets

Server-Side Implementation Requirements

  1. Protect token generation endpoints
    Authenticate users via solutions like Clerk[3] or OAuth before issuing tokens.
  2. Use strong API keys
    Generate keys with:
    • 128+ character length[1]
    • Mixed case letters, numbers, and symbols[1]
  3. Rotate API keys regularly
    Automate rotation cycles (e.g., every 90 days)[1][5]

Client-Side Security Measures

Safe Practices Risks to Avoid
Authentication Token Requests[4][6] Hardcoded API keys[5]
Storage Ephemeral token storage LocalStorage/SessionStorage[5]
Transmission HTTPS only[1][4] Unencrypted HTTP

Additional Best Practices

  • Apply least privilege: Restrict token capabilities to minimum required operations[1][4]
  • Monitor usage: Track anomalous token activity patterns[1][4]
  • Automate secret detection: Use tools like Legit Security to scan for accidental exposures[1]
  • Educate teams: Enforce policies against committing secrets to version control[1][5]

By combining Ably's token authentication system with standard API security practices, you maintain functionality while significantly reducing attack surfaces in client applications[4][6][9].

Citations:


Security Notice: Client-Side Ably API Key Handling

The current use of VITE_PUBLIC_ABLY_KEY exposes an API key directly in client-side code. Ably's best practices advise against using full API keys on the client; instead, Token Authentication should be implemented. Please ensure that:

  • The API key is strictly publishable (i.e., it is not a full-access key) and is limited to non-sensitive operations.
  • You consider transitioning to a Token Authentication strategy where short-lived, scope-limited tokens are generated server-side.
  • Any token generation endpoints are properly secured to avoid exposing sensitive credentials.
examples/liveobjects-live-map/javascript/src/ably.config.d.ts (1)

1-11: LGTM! Well-structured type definitions.

The type definitions are clear, properly scoped, and follow TypeScript best practices. The Tasks type and LiveObjectsTypes interface provide good type safety for the LiveMap implementation.

examples/liveobjects-live-counter/javascript/src/script.ts (1)

12-17: Consider making the client configuration more flexible.

The client configuration is hardcoded with environment: 'sandbox'. This might cause issues when deploying to production.

Consider making the environment configurable through environment variables:

 const client = new Realtime({
   clientId: nanoid(),
   key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string,
-  environment: 'sandbox',
+  environment: import.meta.env.VITE_ABLY_ENVIRONMENT || 'production',
   plugins: { LiveObjects },
 });
examples/liveobjects-live-counter/page.md (2)

1-9: Strong Introduction and Overview
The introduction clearly explains the purpose and use cases of LiveCounter. It provides a solid context for developers.


11-26: Comprehensive API Methods Documentation
The “Resources” section lists the key methods along with documentation links. This is valuable for users to quickly understand how to interact with LiveCounter. Ensure that once the final product documentation is available, these links are updated accordingly.

examples/liveobjects-live-map/page.md (2)

5-9: Well-Outlined LiveMap Features
This section offers a detailed overview of LiveMap’s functionality and potential use cases. It effectively communicates the benefits of using LiveMap for managing shared key/value state.


11-26: Detailed API Reference for LiveMap
The “Resources” section is thorough and mirrors the clarity found in the LiveCounter documentation. The methods are clearly listed with relevant links. Remember to verify these links when final documentation becomes available.

examples/liveobjects-live-map/javascript/vite-env.d.ts (2)

6-8: LGTM! Well-structured type definitions.

The interface correctly extends the global ImportMeta type, providing type-safe access to environment variables while preventing accidental modifications through the readonly modifier.


1-4:

❓ Verification inconclusive

Verify the security implications of exposing the Ably key.

The environment variable name includes "PUBLIC", suggesting this key will be exposed to the client side. Please ensure this is an Ably public/publishable key and not a private key.

Let me help you verify this by checking the Ably documentation:


🌐 Web query:

What is the difference between Ably public and private keys? Are there security implications of exposing Ably keys in client-side code?

💡 Result:

Ably's public and private keys serve distinct roles in authentication and encryption, with critical security implications if mishandled. Here's a breakdown:

Key Differences

  • Private API Keys
    Used for server-side authentication with Ably services[9]. These grant full account access and should never be exposed client-side[4][9].

    • Enable capabilities like channel management and message publishing
    • Required for generating time-limited tokens[9]
  • Public-Facing Tokens
    Temporary credentials derived from private keys via token authentication[9]:

    • Expire after a set time (TTL)
    • Contain granular permissions (e.g., specific channel access)[9]
    • Safe for client-side use when properly configured

Security Risks of Exposing Keys

Exposing Ably private keys in client code creates multiple vulnerabilities[3][4][7]:

Immediate Threats

  • Full account takeover via API key compromise[3]
  • Unauthorized data access to all channels/messages[7]
  • Financial abuse through API quota exhaustion[3][7]

Compliance Impacts

  • GDPR/HIPAA violations from data breaches[2][9]
  • Loss of SOC 2 Type 2 compliance status[2]

Operational Risks

  • Service disruption through DDoS attacks[2][7]
  • Message integrity compromise via payload tampering[4][7]

Mitigation Strategies

Ably recommends these security practices[2][4][9]:

  1. Never embed private keys in client code
    Use server-side token issuance instead[9]. Example secure flow:

    // Server-side token generation
    var capability = new Capability();
    capability.AddResource("channel:chat", Capability.AllowAll);
    var tokenParams = new TokenParams { Ttl = TimeSpan.FromHours(1) };
    var token = await rest.Auth.RequestTokenAsync(tokenParams, capability);
  2. Enable end-to-end encryption
    Add AES encryption to sensitive channels[4]:

    var secretKey = Crypto.GetRandomKey();
    var encryptedChannel = realtime.Get("secure-chat", 
        new ChannelOptions(secretKey));
  3. Implement security controls:

    • IP whitelisting for API endpoints[2]
    • Rate limiting per API key/token[2][9]
    • Regular key rotation schedules[7][9]

For maximum security, Ably's architecture ensures even their engineers cannot decrypt properly configured end-to-end encrypted messages without your private key[2][4]. Always follow principle of least privilege when assigning API key capabilities[9].

Citations:


Security Check: Validate Ably Key Exposure

Based on Ably’s documentation, only public (publishable) tokens are safe for client-side exposure. The environment variable name (VITE_PUBLIC_ABLY_KEY) correctly follows Vite’s naming convention, but please verify that it indeed holds a public token rather than a private API key. Using a private key client-side can lead to severe security risks, including unauthorized access and data breaches.

Action Points:

  • Confirm that VITE_PUBLIC_ABLY_KEY is set to a public Ably key (i.e., a token with restricted permissions) as intended.
  • If the key is private, switch to a secure, server-side token generation flow to mitigate exposure risks.
examples/liveobjects-live-map/javascript/src/script.ts (2)

7-12: Remove hardcoded sandbox environment.

As mentioned in the PR objectives, the sandbox environment configuration should be removed once the channel state feature is available in production.

 const client = new Realtime({
   clientId: nanoid(),
   key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string,
-  environment: 'sandbox',
   plugins: { LiveObjects },
 });

16-17: LGTM! Channel setup looks good.

The channel is correctly configured with appropriate modes for state publishing and subscribing.

Comment on lines +70 to +75
function subscribeToCounterUpdates(color: Color, counter: LiveCounter) {
counter.subscribe(() => {
colorCountDivs[color].innerHTML = counter.value().toString();
});
colorCountDivs[color].innerHTML = counter.value().toString();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add cleanup for counter subscriptions.

The subscriptions to counter updates should be cleaned up to prevent memory leaks.

 function subscribeToCounterUpdates(color: Color, counter: LiveCounter) {
-  counter.subscribe(() => {
+  const subscription = counter.subscribe(() => {
     colorCountDivs[color].innerHTML = counter.value().toString();
   });
   colorCountDivs[color].innerHTML = counter.value().toString();
+  return subscription;  // Return subscription for cleanup
 }

And update the calling code to handle cleanup:

// Store subscriptions for cleanup
const subscriptions = new Map();

// In initCounters:
const subscription = subscribeToCounterUpdates(color, counter);
subscriptions.set(color, subscription);

// Add cleanup function:
function cleanup() {
  subscriptions.forEach(subscription => subscription.unsubscribe());
  subscriptions.clear();
}

Comment on lines +51 to +70
function subscribeToTasksUpdates(tasks: Tasks) {
tasksDiv.innerHTML = '';

tasks.subscribe(({ update }) => {
Object.entries(update).forEach(async ([taskId, change]) => {
switch (change) {
case 'updated':
tasksOnUpdated(taskId, tasks);
break;
case 'removed':
tasksOnRemoved(taskId);
break;
}
});
});

for (const [taskId] of tasks.entries()) {
createTaskDiv({ id: taskId, title: tasks.get(taskId) }, tasks);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add cleanup for task subscriptions.

The subscription should be cleaned up when no longer needed to prevent memory leaks and unexpected behavior.

 function subscribeToTasksUpdates(tasks: Tasks) {
   tasksDiv.innerHTML = '';

-  tasks.subscribe(({ update }) => {
+  const subscription = tasks.subscribe(({ update }) => {
     Object.entries(update).forEach(async ([taskId, change]) => {
       switch (change) {
         case 'updated':
           tasksOnUpdated(taskId, tasks);
           break;
         case 'removed':
           tasksOnRemoved(taskId);
           break;
       }
     });
   });

   for (const [taskId] of tasks.entries()) {
     createTaskDiv({ id: taskId, title: tasks.get(taskId) }, tasks);
   }
+
+  return () => subscription.unsubscribe();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function subscribeToTasksUpdates(tasks: Tasks) {
tasksDiv.innerHTML = '';
tasks.subscribe(({ update }) => {
Object.entries(update).forEach(async ([taskId, change]) => {
switch (change) {
case 'updated':
tasksOnUpdated(taskId, tasks);
break;
case 'removed':
tasksOnRemoved(taskId);
break;
}
});
});
for (const [taskId] of tasks.entries()) {
createTaskDiv({ id: taskId, title: tasks.get(taskId) }, tasks);
}
}
function subscribeToTasksUpdates(tasks: Tasks) {
tasksDiv.innerHTML = '';
const subscription = tasks.subscribe(({ update }) => {
Object.entries(update).forEach(async ([taskId, change]) => {
switch (change) {
case 'updated':
tasksOnUpdated(taskId, tasks);
break;
case 'removed':
tasksOnRemoved(taskId);
break;
}
});
});
for (const [taskId] of tasks.entries()) {
createTaskDiv({ id: taskId, title: tasks.get(taskId) }, tasks);
}
return () => subscription.unsubscribe();
}

@VeskeR
Copy link
Contributor Author

VeskeR commented Feb 24, 2025

Screenshot of the apps:

Live Map counter example is a simple todo board:
livemap

Live Counter example is a voting app:
livecounter

@VeskeR VeskeR force-pushed the PUB-1135/liveobjects-example branch from 09e9bd1 to 9f4cfeb Compare February 24, 2025 11:44
@VeskeR
Copy link
Contributor Author

VeskeR commented Mar 11, 2025

@GregHolmes I haven't added the React examples as we don't have the react hooks for liveobjects ready and I didn't want to spend time on preparing an example with React and plain liveobjects js which soon will need to be rewritten anyway.

Does the new examples structure allow for React variant to be missing? Or it has to be present?

@GregHolmes GregHolmes force-pushed the PUB-1135/liveobjects-example branch from 9f4cfeb to 9f40624 Compare March 19, 2025 10:53
@VeskeR VeskeR force-pushed the PUB-1135/liveobjects-example branch from 7db22da to 2ca853f Compare March 21, 2025 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants