npm package: @superapp_men/device-storage
Description: Key-value device storage for SuperApp partner applications via iframe/Capacitor
Author: SuperApp Team
Latest version: 1.0.0

# @superapp_men/device-storage

**Version:** 1.0.0
**License:** MIT
**Author:** SuperApp Team

Package for **partner applications** (running inside the SuperApp iframe or Capacitor WebView) to perform key-value storage operations on the device. The SuperApp handles actual storage, auto-namespaces keys per partner (e.g. `bewize:myKey`), and returns results via postMessage.

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Architecture Overview](#architecture-overview)
- [Client API](#client-api)
  - [DeviceStorageClient](#devicestorageclient)
  - [Methods](#methods)
- [Data Dictionary](#data-dictionary)
  - [StorageRequestPayload (per action)](#storagerequestpayload-per-action)
  - [StorageResponseBody (per action)](#storageresponsebody-per-action)
  - [StorageErrorBody](#storageerrorbody)
- [Validation Rules](#validation-rules)
- [Usage Examples](#usage-examples)
- [Communication Protocol](#communication-protocol)
- [SuperApp Integration](#superapp-integration)
- [Exported Members](#exported-members)
- [Constants](#constants)

---

## Features

- **Simple async API** — `get`, `set`, `remove`, `has`, `keys`, `clear` with Promise-based returns.
- **Client-side validation** before calling the SuperApp: key format, value size, and partner code are validated locally.
- **Auto-namespacing** — The SuperApp prefixes all keys with the partner code (e.g. `bewize:myKey`), preventing collisions between partner apps.
- **Dual transport support** — works in both iframe (postMessage to parent) and Capacitor WebView environments.
- **Type-safe** — Full TypeScript types for all operations, payloads, and responses.

---

## Installation

```bash
npm install @superapp_men/device-storage
```

---

## Architecture Overview

```
Partner App (iframe / Capacitor WebView)
  |
  +-- DeviceStorageClient          -> validates inputs, sends via bridge
        |
        +-- 1. Client-side validation (key, value, partnerCode)
        |     +-- If invalid -> throws Error immediately
        |
        +-- 2. If valid -> sends postMessage to SuperApp
              |
              SuperApp (host)
              +-- Listens for "storage:request"
              +-- Namespaces key with partnerCode (e.g. "bewize:myKey")
              +-- Performs storage operation (localStorage, SQLite, etc.)
              +-- Returns "storage:response" with result
```

---

## Client API

### `DeviceStorageClient`

```ts
import { DeviceStorageClient } from "@superapp_men/device-storage";

const storage = new DeviceStorageClient({
  partnerCode: "bewize",
  timeout: 10000,
  debug: true,
});
```

**Constructor config (`DeviceStorageClientConfig`):**

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `partnerCode` | `string` | Yes | — | Partner application code. Must be one of: `"bewize"`, `"ltm"`, `"cantoo"`, `"ekinox"`, `"math-scan"` (case-insensitive). Validated at construction time. |
| `timeout` | `number` | No | `10000` | Request timeout in milliseconds. |
| `debug` | `boolean` | No | `false` | Enable debug console logging. |

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `get(key)` | `Promise<string \| null>` | Retrieve a value by key. Returns `null` if the key does not exist. |
| `set(key, value)` | `Promise<void>` | Store a key-value pair. |
| `remove(key)` | `Promise<void>` | Delete a key. |
| `has(key)` | `Promise<boolean>` | Check if a key exists. |
| `keys()` | `Promise<string[]>` | List all keys for this partner (unprefixed). |
| `clear()` | `Promise<void>` | Clear all keys for this partner. |
| `setDebug(enabled)` | `void` | Toggle debug logging at runtime. |
| `destroy()` | `void` | Clean up listeners and pending timeouts. Call when done. |

**Error handling:** All methods throw an `Error` if:
- Client-side validation fails (invalid key, value, or partnerCode).
- The SuperApp returns a non-2xx status code.
- The request times out.
- No target window is found (not in iframe or Capacitor).

---

## Data Dictionary

### `StorageRequestPayload` (per action)

Each request includes an `action` field that determines the payload shape:

| Action | Payload Fields | Description |
|--------|---------------|-------------|
| `"get"` | `{ action: "get", key: string }` | Retrieve a value. |
| `"set"` | `{ action: "set", key: string, value: string }` | Store a value. |
| `"remove"` | `{ action: "remove", key: string }` | Delete a key. |
| `"has"` | `{ action: "has", key: string }` | Check existence. |
| `"keys"` | `{ action: "keys" }` | List all keys. |
| `"clear"` | `{ action: "clear" }` | Clear all keys. |

The `partnerCode` is automatically appended to the payload by the bridge before sending.

### `StorageResponseBody` (per action)

| Action | Response Body | Description |
|--------|--------------|-------------|
| `"get"` | `{ value: string \| null }` | The stored value, or `null` if key doesn't exist. |
| `"set"` | `{ success: true }` | Confirmation of successful write. |
| `"remove"` | `{ success: true }` | Confirmation of successful delete. |
| `"has"` | `{ exists: boolean }` | Whether the key exists. |
| `"keys"` | `{ keys: string[] }` | Array of unprefixed keys for this partner. |
| `"clear"` | `{ success: true }` | Confirmation of successful clear. |

### `StorageErrorBody`

Returned by the SuperApp when an operation fails (non-2xx status).

| Field | Type | Description |
|-------|------|-------------|
| `error` | `string` | Error message. |
| `detail` | `string?` | Optional detailed error information. |

---

## Validation Rules

All validation runs client-side **before** sending to the SuperApp.

### Partner Code Validation

| Rule | Error Message |
|------|---------------|
| Required, non-empty string | `"partnerCode is required."` |
| Must be one of: `bewize`, `ltm`, `cantoo`, `ekinox`, `math-scan` (case-insensitive) | `"partnerCode must be one of: bewize, ltm, cantoo, ekinox, math-scan."` |

### Key Validation

| Rule | Error Message |
|------|---------------|
| Required, non-empty string | `"key is required and must be a non-empty string."` |
| Max 256 characters | `"key must not exceed 256 characters."` |
| No invalid characters (`< > : " / \ \| ? *` and control characters) | `"key contains invalid characters. Avoid: < > : \" / \\ \| ? * and control characters."` |

### Value Validation (for `set()` only)

| Rule | Error Message |
|------|---------------|
| Required, must be a string | `"value is required for set()."` / `"value must be a string."` |
| Max 1 MB (1,048,576 bytes UTF-8) | `"value must not exceed 1048576 bytes (1 MB)."` |

---

## Usage Examples

### Basic CRUD Operations

```ts
import { DeviceStorageClient } from "@superapp_men/device-storage";

const storage = new DeviceStorageClient({
  partnerCode: "bewize",
  timeout: 10000,
  debug: false,
});

// Store a value
await storage.set("user-preference", "dark-mode");

// Retrieve a value
const value = await storage.get("user-preference");
console.log(value); // "dark-mode"

// Check if a key exists
const exists = await storage.has("user-preference");
console.log(exists); // true

// Delete a key
await storage.remove("user-preference");

// Clean up when done
storage.destroy();
```

### Listing and Clearing Keys

```ts
// Store multiple values
await storage.set("theme", "dark");
await storage.set("language", "fr");
await storage.set("font-size", "16");

// List all keys for this partner
const allKeys = await storage.keys();
console.log(allKeys); // ["theme", "language", "font-size"]

// Clear all keys for this partner
await storage.clear();

const afterClear = await storage.keys();
console.log(afterClear); // []
```

### Storing Complex Data (JSON)

```ts
const userData = { name: "Alice", score: 95, level: 3 };
await storage.set("user-data", JSON.stringify(userData));

const raw = await storage.get("user-data");
if (raw !== null) {
  const parsed = JSON.parse(raw);
  console.log(parsed.name); // "Alice"
}
```

### Error Handling

```ts
try {
  await storage.get("my-key");
} catch (error) {
  if (error instanceof Error) {
    console.error("Storage error:", error.message);
    // Possible messages:
    // - "key is required and must be a non-empty string."  (validation)
    // - "Storage get failed"                                (SuperApp error)
    // - "Device storage request timeout after 10000ms"      (timeout)
    // - "Cannot find target window (not in iframe or Capacitor?)" (environment)
  }
}
```

---

## Communication Protocol

### Message Types

| Enum | Value | Direction | Description |
|------|-------|-----------|-------------|
| `StorageMessageType.REQUEST` | `"storage:request"` | Partner -> SuperApp | Partner sends a storage operation. |
| `StorageMessageType.RESPONSE` | `"storage:response"` | SuperApp -> Partner | SuperApp returns the result. |

### Request Message (`StorageRequestMessage`)

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"storage:request"` | Message type constant. |
| `requestId` | `string` | Unique ID for matching the response. |
| `payload` | `StorageRequestPayload & { partnerCode: string }` | The action payload with partner code. |
| `timestamp` | `number` | `Date.now()` at time of sending. |

**Example request (set):**
```json
{
  "type": "storage:request",
  "requestId": "1709123456789-abc123def",
  "payload": {
    "action": "set",
    "key": "theme",
    "value": "dark",
    "partnerCode": "bewize"
  },
  "timestamp": 1709123456789
}
```

### Response Message (`StorageResponseMessage`)

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"storage:response"` | Message type constant. |
| `requestId` | `string` | Matches the request's `requestId`. |
| `statusCode` | `number` | HTTP-style status code (200 = success, 400 = error, 500 = internal). |
| `body` | `StorageResponseBody \| StorageErrorBody` | Result or error body. |
| `timestamp` | `number` | `Date.now()` at time of response. |

**Example response (set success):**
```json
{
  "type": "storage:response",
  "requestId": "1709123456789-abc123def",
  "statusCode": 200,
  "body": { "success": true },
  "timestamp": 1709123456790
}
```

**Example response (get):**
```json
{
  "type": "storage:response",
  "requestId": "1709123456789-abc123def",
  "statusCode": 200,
  "body": { "value": "dark" },
  "timestamp": 1709123456790
}
```

---

## SuperApp Integration

The SuperApp must:

1. Listen for messages with `type: "storage:request"`.
2. Read `payload.partnerCode` and `payload.action`.
3. Namespace the key: `${partnerCode}:${key}` (e.g. `bewize:theme`).
4. Perform the storage operation (localStorage, Capacitor Preferences, SQLite, etc.).
5. Send response via postMessage with `type: "storage:response"`, matching `requestId`.

**Key namespacing:** For `keys()`, return only keys matching the partner prefix and strip the prefix. For `clear()`, delete only keys matching the partner prefix.

**SuperApp imports:**

```ts
import type {
  StorageRequestMessage,
  StorageResponseMessage,
  StorageRequestPayload,
} from "@superapp_men/device-storage/superapp";
import { StorageMessageType } from "@superapp_men/device-storage/superapp";
```

**Example SuperApp handler:**

```ts
window.addEventListener("message", async (event) => {
  const msg = event.data;
  if (msg?.type !== StorageMessageType.REQUEST) return;

  const { requestId, payload, timestamp } = msg as StorageRequestMessage;
  const prefix = `${payload.partnerCode}:`;

  let statusCode = 200;
  let body: unknown;

  try {
    switch (payload.action) {
      case "get":
        body = { value: localStorage.getItem(prefix + payload.key) };
        break;
      case "set":
        localStorage.setItem(prefix + payload.key, payload.value);
        body = { success: true };
        break;
      case "remove":
        localStorage.removeItem(prefix + payload.key);
        body = { success: true };
        break;
      case "has":
        body = { exists: localStorage.getItem(prefix + payload.key) !== null };
        break;
      case "keys": {
        const allKeys: string[] = [];
        for (let i = 0; i < localStorage.length; i++) {
          const k = localStorage.key(i);
          if (k?.startsWith(prefix)) allKeys.push(k.slice(prefix.length));
        }
        body = { keys: allKeys };
        break;
      }
      case "clear":
        for (let i = localStorage.length - 1; i >= 0; i--) {
          const k = localStorage.key(i);
          if (k?.startsWith(prefix)) localStorage.removeItem(k);
        }
        body = { success: true };
        break;
    }
  } catch (err) {
    statusCode = 500;
    body = { error: "Internal storage error", detail: String(err) };
  }

  event.source?.postMessage(
    { type: StorageMessageType.RESPONSE, requestId, statusCode, body, timestamp: Date.now() },
    { targetOrigin: event.origin },
  );
});
```

---

## Exported Members

### Main entry (`@superapp_men/device-storage`)

| Export | Kind | Description |
|--------|------|-------------|
| `DeviceStorageClient` | Class | Main client for storage operations. |
| `DeviceStorageClientConfig` | Type | Constructor config interface. |
| `StorageMessageType` | Enum | `REQUEST`, `RESPONSE` message type constants. |
| `StorageAction` | Type | `"get" \| "set" \| "remove" \| "has" \| "keys" \| "clear"`. |
| `StorageRequestPayload` | Type | Union of all action payloads. |
| `StorageGetPayload` | Type | `{ action: "get", key }`. |
| `StorageSetPayload` | Type | `{ action: "set", key, value }`. |
| `StorageRemovePayload` | Type | `{ action: "remove", key }`. |
| `StorageHasPayload` | Type | `{ action: "has", key }`. |
| `StorageKeysPayload` | Type | `{ action: "keys" }`. |
| `StorageClearPayload` | Type | `{ action: "clear" }`. |
| `StorageResponseBody` | Type | Union of all response bodies. |
| `StorageGetResponseBody` | Type | `{ value: string \| null }`. |
| `StorageSetResponseBody` | Type | `{ success: true }`. |
| `StorageRemoveResponseBody` | Type | `{ success: true }`. |
| `StorageHasResponseBody` | Type | `{ exists: boolean }`. |
| `StorageKeysResponseBody` | Type | `{ keys: string[] }`. |
| `StorageClearResponseBody` | Type | `{ success: true }`. |
| `StorageErrorBody` | Type | `{ error: string, detail?: string }`. |
| `StorageRequestMessage` | Type | postMessage request structure. |
| `StorageResponseMessage` | Type | postMessage response structure. |
| `StorageApiResult` | Type | Generic result type from bridge. |
| `validatePartnerCode` | Function | Validate partner code. |
| `validateKey` | Function | Validate storage key. |
| `validateValue` | Function | Validate storage value. |
| `ValidationResult` | Type | `{ valid: boolean, errors: ValidationError[] }`. |
| `ValidationError` | Type | `{ key: string, message: string }`. |

### SuperApp entry (`@superapp_men/device-storage/superapp`)

| Export | Kind | Description |
|--------|------|-------------|
| `StorageMessageType` | Enum | Message type constants. |
| All payload/response types | Types | Same type exports as main entry (types only, no client/bridge/validator). |

---

## Constants

| Constant | Value | Used In |
|----------|-------|---------|
| `MAX_KEY_LENGTH` | `256` | Key max length validation. |
| `MAX_VALUE_SIZE` | `1,048,576` (1 MB) | Value max byte size validation. |
| `VALID_PARTNER_CODES` | `["bewize", "ltm", "cantoo", "ekinox", "math-scan"]` | Allowed partner code values (case-insensitive). |
| Default `timeout` | `10,000` ms | Client request timeout. |