@web-remarq/cloud 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,22 @@ SHA-256 hex (64 chars) of the key. This is what the database stores.
84
84
  - No web dashboard — manage annotations via Supabase Studio for now
85
85
  - Two tabs editing the same annotation: last write wins
86
86
 
87
+ ## Upgrading from 0.1.x
88
+
89
+ `@web-remarq/cloud@0.2.0` adds a `lifecycle` column to the `annotations` table
90
+ for full audit-trail persistence (core v0.7.0 feature). Run the additive
91
+ migration in your Supabase SQL Editor **before** installing the new version:
92
+
93
+ ```sql
94
+ -- packages/cloud/sql/002_lifecycle.sql
95
+ alter table annotations
96
+ add column lifecycle jsonb not null default '[]'::jsonb;
97
+ ```
98
+
99
+ The migration is safe to run on production data. Existing rows get an empty
100
+ array; the core's `migrateAnnotation` synthesizes a `created` event on load
101
+ when lifecycle is empty.
102
+
87
103
  ## License
88
104
 
89
105
  MIT
package/dist/index.cjs CHANGED
@@ -53,6 +53,7 @@ function annotationToRow(a) {
53
53
  comment: a.comment,
54
54
  status: a.status,
55
55
  timestamp_ms: a.timestamp,
56
+ lifecycle: a.lifecycle,
56
57
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
57
58
  };
58
59
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cloud-storage-adapter.ts","../src/project-key.ts"],"sourcesContent":["import type { StorageAdapter } from 'web-remarq'\nimport { CloudStorageAdapter } from './cloud-storage-adapter'\nimport type { CloudStorageOptions } from './types'\n\nexport { generateProjectKey, hashProjectKey } from './project-key'\nexport { CloudStorageAdapter } from './cloud-storage-adapter'\nexport type { CloudStorageOptions } from './types'\n\nexport function createCloudStorage(opts: CloudStorageOptions): StorageAdapter {\n return new CloudStorageAdapter(opts)\n}\n","import { createClient, type SupabaseClient } from '@supabase/supabase-js'\nimport type {\n Annotation,\n AnnotationEvent,\n AnnotationStatus,\n AnnotationStore,\n ElementFingerprint,\n StorageAdapter,\n} from 'web-remarq'\nimport type { CloudStorageOptions } from './types'\n\ninterface AnnotationRow {\n id: string\n project_id?: string\n route: string\n viewport: string\n viewport_bucket: number\n fingerprint: ElementFingerprint\n comment: string\n status: AnnotationStatus\n timestamp_ms: number\n lifecycle?: AnnotationEvent[]\n created_at?: string\n updated_at?: string\n}\n\ntype AnnotationWriteRow = Omit<AnnotationRow, 'project_id' | 'created_at'>\n\nfunction rowToAnnotation(row: AnnotationRow): Annotation {\n return {\n id: row.id,\n comment: row.comment,\n fingerprint: row.fingerprint,\n route: row.route,\n viewport: row.viewport,\n viewportBucket: row.viewport_bucket,\n timestamp: row.timestamp_ms,\n status: row.status,\n lifecycle: row.lifecycle ?? [],\n }\n}\n\nfunction annotationToRow(a: Annotation): AnnotationWriteRow {\n // lifecycle is intentionally NOT written here — cloud-0.1.0 schema doesn't\n // have a lifecycle column. Will be added in cloud-0.1.1 alongside SQL migration.\n // Until then, lifecycle history doesn't survive across browser sessions for\n // cloud users; migrateAnnotation synthesizes a created event on load.\n return {\n id: a.id,\n route: a.route,\n viewport: a.viewport,\n viewport_bucket: a.viewportBucket,\n fingerprint: a.fingerprint,\n comment: a.comment,\n status: a.status,\n timestamp_ms: a.timestamp,\n updated_at: new Date().toISOString(),\n }\n}\n\nexport class CloudStorageAdapter implements StorageAdapter {\n readonly isMemoryOnly = false\n private client: SupabaseClient\n private onError: 'throw' | 'memory-fallback'\n\n constructor(opts: CloudStorageOptions) {\n this.onError = opts.onError ?? 'throw'\n this.client = createClient(opts.supabaseUrl, opts.supabaseAnonKey, {\n global: {\n headers: { 'x-remarq-project-key': opts.projectKey },\n },\n auth: { persistSession: false },\n })\n }\n\n async load(): Promise<AnnotationStore> {\n const { data, error } = await this.client\n .from('annotations')\n .select('*')\n .order('timestamp_ms', { ascending: true })\n\n if (error) {\n return this.handleError<AnnotationStore>(error, { version: 1, annotations: [] })\n }\n\n const rows = (data ?? []) as AnnotationRow[]\n return { version: 1, annotations: rows.map(rowToAnnotation) }\n }\n\n async save(annotation: Annotation): Promise<void> {\n const row = annotationToRow(annotation)\n const { error } = await this.client\n .from('annotations')\n .upsert(row, { onConflict: 'id' })\n if (error) this.handleError<void>(error, undefined)\n }\n\n async remove(id: string): Promise<void> {\n const { error } = await this.client.from('annotations').delete().eq('id', id)\n if (error) this.handleError<void>(error, undefined)\n }\n\n async clear(): Promise<void> {\n const { error } = await this.client\n .from('annotations')\n .delete()\n .neq('id', '__never_matches__')\n if (error) this.handleError<void>(error, undefined)\n }\n\n private handleError<T>(error: unknown, fallback: T): T {\n if (this.onError === 'throw') {\n throw error\n }\n console.warn('[web-remarq cloud]', error)\n return fallback\n }\n}\n","const PREFIX = 'pk_'\nconst KEY_LENGTH = 32\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\nexport function generateProjectKey(): string {\n const bytes = new Uint8Array(KEY_LENGTH)\n crypto.getRandomValues(bytes)\n let key = PREFIX\n for (let i = 0; i < KEY_LENGTH; i++) {\n key += ALPHABET[bytes[i] % ALPHABET.length]\n }\n return key\n}\n\nexport async function hashProjectKey(key: string): Promise<string> {\n const data = new TextEncoder().encode(key)\n const buf = await crypto.subtle.digest('SHA-256', data)\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cloud-storage-adapter.ts","../src/project-key.ts"],"sourcesContent":["import type { StorageAdapter } from 'web-remarq'\nimport { CloudStorageAdapter } from './cloud-storage-adapter'\nimport type { CloudStorageOptions } from './types'\n\nexport { generateProjectKey, hashProjectKey } from './project-key'\nexport { CloudStorageAdapter } from './cloud-storage-adapter'\nexport type { CloudStorageOptions } from './types'\n\nexport function createCloudStorage(opts: CloudStorageOptions): StorageAdapter {\n return new CloudStorageAdapter(opts)\n}\n","import { createClient, type SupabaseClient } from '@supabase/supabase-js'\nimport type {\n Annotation,\n AnnotationEvent,\n AnnotationStatus,\n AnnotationStore,\n ElementFingerprint,\n StorageAdapter,\n} from 'web-remarq'\nimport type { CloudStorageOptions } from './types'\n\ninterface AnnotationRow {\n id: string\n project_id?: string\n route: string\n viewport: string\n viewport_bucket: number\n fingerprint: ElementFingerprint\n comment: string\n status: AnnotationStatus\n timestamp_ms: number\n lifecycle: AnnotationEvent[]\n created_at?: string\n updated_at?: string\n}\n\ntype AnnotationWriteRow = Omit<AnnotationRow, 'project_id' | 'created_at'>\n\nfunction rowToAnnotation(row: AnnotationRow): Annotation {\n return {\n id: row.id,\n comment: row.comment,\n fingerprint: row.fingerprint,\n route: row.route,\n viewport: row.viewport,\n viewportBucket: row.viewport_bucket,\n timestamp: row.timestamp_ms,\n status: row.status,\n lifecycle: row.lifecycle ?? [],\n }\n}\n\nfunction annotationToRow(a: Annotation): AnnotationWriteRow {\n return {\n id: a.id,\n route: a.route,\n viewport: a.viewport,\n viewport_bucket: a.viewportBucket,\n fingerprint: a.fingerprint,\n comment: a.comment,\n status: a.status,\n timestamp_ms: a.timestamp,\n lifecycle: a.lifecycle,\n updated_at: new Date().toISOString(),\n }\n}\n\nexport class CloudStorageAdapter implements StorageAdapter {\n readonly isMemoryOnly = false\n private client: SupabaseClient\n private onError: 'throw' | 'memory-fallback'\n\n constructor(opts: CloudStorageOptions) {\n this.onError = opts.onError ?? 'throw'\n this.client = createClient(opts.supabaseUrl, opts.supabaseAnonKey, {\n global: {\n headers: { 'x-remarq-project-key': opts.projectKey },\n },\n auth: { persistSession: false },\n })\n }\n\n async load(): Promise<AnnotationStore> {\n const { data, error } = await this.client\n .from('annotations')\n .select('*')\n .order('timestamp_ms', { ascending: true })\n\n if (error) {\n return this.handleError<AnnotationStore>(error, { version: 1, annotations: [] })\n }\n\n const rows = (data ?? []) as AnnotationRow[]\n return { version: 1, annotations: rows.map(rowToAnnotation) }\n }\n\n async save(annotation: Annotation): Promise<void> {\n const row = annotationToRow(annotation)\n const { error } = await this.client\n .from('annotations')\n .upsert(row, { onConflict: 'id' })\n if (error) this.handleError<void>(error, undefined)\n }\n\n async remove(id: string): Promise<void> {\n const { error } = await this.client.from('annotations').delete().eq('id', id)\n if (error) this.handleError<void>(error, undefined)\n }\n\n async clear(): Promise<void> {\n const { error } = await this.client\n .from('annotations')\n .delete()\n .neq('id', '__never_matches__')\n if (error) this.handleError<void>(error, undefined)\n }\n\n private handleError<T>(error: unknown, fallback: T): T {\n if (this.onError === 'throw') {\n throw error\n }\n console.warn('[web-remarq cloud]', error)\n return fallback\n }\n}\n","const PREFIX = 'pk_'\nconst KEY_LENGTH = 32\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\nexport function generateProjectKey(): string {\n const bytes = new Uint8Array(KEY_LENGTH)\n crypto.getRandomValues(bytes)\n let key = PREFIX\n for (let i = 0; i < KEY_LENGTH; i++) {\n key += ALPHABET[bytes[i] % ALPHABET.length]\n }\n return key\n}\n\nexport async function hashProjectKey(key: string): Promise<string> {\n const data = new TextEncoder().encode(key)\n const buf = await crypto.subtle.digest('SHA-256', data)\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ function annotationToRow(a) {
24
24
  comment: a.comment,
25
25
  status: a.status,
26
26
  timestamp_ms: a.timestamp,
27
+ lifecycle: a.lifecycle,
27
28
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
28
29
  };
29
30
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cloud-storage-adapter.ts","../src/project-key.ts","../src/index.ts"],"sourcesContent":["import { createClient, type SupabaseClient } from '@supabase/supabase-js'\nimport type {\n Annotation,\n AnnotationEvent,\n AnnotationStatus,\n AnnotationStore,\n ElementFingerprint,\n StorageAdapter,\n} from 'web-remarq'\nimport type { CloudStorageOptions } from './types'\n\ninterface AnnotationRow {\n id: string\n project_id?: string\n route: string\n viewport: string\n viewport_bucket: number\n fingerprint: ElementFingerprint\n comment: string\n status: AnnotationStatus\n timestamp_ms: number\n lifecycle?: AnnotationEvent[]\n created_at?: string\n updated_at?: string\n}\n\ntype AnnotationWriteRow = Omit<AnnotationRow, 'project_id' | 'created_at'>\n\nfunction rowToAnnotation(row: AnnotationRow): Annotation {\n return {\n id: row.id,\n comment: row.comment,\n fingerprint: row.fingerprint,\n route: row.route,\n viewport: row.viewport,\n viewportBucket: row.viewport_bucket,\n timestamp: row.timestamp_ms,\n status: row.status,\n lifecycle: row.lifecycle ?? [],\n }\n}\n\nfunction annotationToRow(a: Annotation): AnnotationWriteRow {\n // lifecycle is intentionally NOT written here — cloud-0.1.0 schema doesn't\n // have a lifecycle column. Will be added in cloud-0.1.1 alongside SQL migration.\n // Until then, lifecycle history doesn't survive across browser sessions for\n // cloud users; migrateAnnotation synthesizes a created event on load.\n return {\n id: a.id,\n route: a.route,\n viewport: a.viewport,\n viewport_bucket: a.viewportBucket,\n fingerprint: a.fingerprint,\n comment: a.comment,\n status: a.status,\n timestamp_ms: a.timestamp,\n updated_at: new Date().toISOString(),\n }\n}\n\nexport class CloudStorageAdapter implements StorageAdapter {\n readonly isMemoryOnly = false\n private client: SupabaseClient\n private onError: 'throw' | 'memory-fallback'\n\n constructor(opts: CloudStorageOptions) {\n this.onError = opts.onError ?? 'throw'\n this.client = createClient(opts.supabaseUrl, opts.supabaseAnonKey, {\n global: {\n headers: { 'x-remarq-project-key': opts.projectKey },\n },\n auth: { persistSession: false },\n })\n }\n\n async load(): Promise<AnnotationStore> {\n const { data, error } = await this.client\n .from('annotations')\n .select('*')\n .order('timestamp_ms', { ascending: true })\n\n if (error) {\n return this.handleError<AnnotationStore>(error, { version: 1, annotations: [] })\n }\n\n const rows = (data ?? []) as AnnotationRow[]\n return { version: 1, annotations: rows.map(rowToAnnotation) }\n }\n\n async save(annotation: Annotation): Promise<void> {\n const row = annotationToRow(annotation)\n const { error } = await this.client\n .from('annotations')\n .upsert(row, { onConflict: 'id' })\n if (error) this.handleError<void>(error, undefined)\n }\n\n async remove(id: string): Promise<void> {\n const { error } = await this.client.from('annotations').delete().eq('id', id)\n if (error) this.handleError<void>(error, undefined)\n }\n\n async clear(): Promise<void> {\n const { error } = await this.client\n .from('annotations')\n .delete()\n .neq('id', '__never_matches__')\n if (error) this.handleError<void>(error, undefined)\n }\n\n private handleError<T>(error: unknown, fallback: T): T {\n if (this.onError === 'throw') {\n throw error\n }\n console.warn('[web-remarq cloud]', error)\n return fallback\n }\n}\n","const PREFIX = 'pk_'\nconst KEY_LENGTH = 32\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\nexport function generateProjectKey(): string {\n const bytes = new Uint8Array(KEY_LENGTH)\n crypto.getRandomValues(bytes)\n let key = PREFIX\n for (let i = 0; i < KEY_LENGTH; i++) {\n key += ALPHABET[bytes[i] % ALPHABET.length]\n }\n return key\n}\n\nexport async function hashProjectKey(key: string): Promise<string> {\n const data = new TextEncoder().encode(key)\n const buf = await crypto.subtle.digest('SHA-256', data)\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n","import type { StorageAdapter } from 'web-remarq'\nimport { CloudStorageAdapter } from './cloud-storage-adapter'\nimport type { CloudStorageOptions } from './types'\n\nexport { generateProjectKey, hashProjectKey } from './project-key'\nexport { CloudStorageAdapter } from './cloud-storage-adapter'\nexport type { CloudStorageOptions } from './types'\n\nexport function createCloudStorage(opts: CloudStorageOptions): StorageAdapter {\n return new CloudStorageAdapter(opts)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cloud-storage-adapter.ts","../src/project-key.ts","../src/index.ts"],"sourcesContent":["import { createClient, type SupabaseClient } from '@supabase/supabase-js'\nimport type {\n Annotation,\n AnnotationEvent,\n AnnotationStatus,\n AnnotationStore,\n ElementFingerprint,\n StorageAdapter,\n} from 'web-remarq'\nimport type { CloudStorageOptions } from './types'\n\ninterface AnnotationRow {\n id: string\n project_id?: string\n route: string\n viewport: string\n viewport_bucket: number\n fingerprint: ElementFingerprint\n comment: string\n status: AnnotationStatus\n timestamp_ms: number\n lifecycle: AnnotationEvent[]\n created_at?: string\n updated_at?: string\n}\n\ntype AnnotationWriteRow = Omit<AnnotationRow, 'project_id' | 'created_at'>\n\nfunction rowToAnnotation(row: AnnotationRow): Annotation {\n return {\n id: row.id,\n comment: row.comment,\n fingerprint: row.fingerprint,\n route: row.route,\n viewport: row.viewport,\n viewportBucket: row.viewport_bucket,\n timestamp: row.timestamp_ms,\n status: row.status,\n lifecycle: row.lifecycle ?? [],\n }\n}\n\nfunction annotationToRow(a: Annotation): AnnotationWriteRow {\n return {\n id: a.id,\n route: a.route,\n viewport: a.viewport,\n viewport_bucket: a.viewportBucket,\n fingerprint: a.fingerprint,\n comment: a.comment,\n status: a.status,\n timestamp_ms: a.timestamp,\n lifecycle: a.lifecycle,\n updated_at: new Date().toISOString(),\n }\n}\n\nexport class CloudStorageAdapter implements StorageAdapter {\n readonly isMemoryOnly = false\n private client: SupabaseClient\n private onError: 'throw' | 'memory-fallback'\n\n constructor(opts: CloudStorageOptions) {\n this.onError = opts.onError ?? 'throw'\n this.client = createClient(opts.supabaseUrl, opts.supabaseAnonKey, {\n global: {\n headers: { 'x-remarq-project-key': opts.projectKey },\n },\n auth: { persistSession: false },\n })\n }\n\n async load(): Promise<AnnotationStore> {\n const { data, error } = await this.client\n .from('annotations')\n .select('*')\n .order('timestamp_ms', { ascending: true })\n\n if (error) {\n return this.handleError<AnnotationStore>(error, { version: 1, annotations: [] })\n }\n\n const rows = (data ?? []) as AnnotationRow[]\n return { version: 1, annotations: rows.map(rowToAnnotation) }\n }\n\n async save(annotation: Annotation): Promise<void> {\n const row = annotationToRow(annotation)\n const { error } = await this.client\n .from('annotations')\n .upsert(row, { onConflict: 'id' })\n if (error) this.handleError<void>(error, undefined)\n }\n\n async remove(id: string): Promise<void> {\n const { error } = await this.client.from('annotations').delete().eq('id', id)\n if (error) this.handleError<void>(error, undefined)\n }\n\n async clear(): Promise<void> {\n const { error } = await this.client\n .from('annotations')\n .delete()\n .neq('id', '__never_matches__')\n if (error) this.handleError<void>(error, undefined)\n }\n\n private handleError<T>(error: unknown, fallback: T): T {\n if (this.onError === 'throw') {\n throw error\n }\n console.warn('[web-remarq cloud]', error)\n return fallback\n }\n}\n","const PREFIX = 'pk_'\nconst KEY_LENGTH = 32\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\nexport function generateProjectKey(): string {\n const bytes = new Uint8Array(KEY_LENGTH)\n crypto.getRandomValues(bytes)\n let key = PREFIX\n for (let i = 0; i < KEY_LENGTH; i++) {\n key += ALPHABET[bytes[i] % ALPHABET.length]\n }\n return key\n}\n\nexport async function hashProjectKey(key: string): Promise<string> {\n const data = new TextEncoder().encode(key)\n const buf = await crypto.subtle.digest('SHA-256', data)\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n","import type { StorageAdapter } from 'web-remarq'\nimport { CloudStorageAdapter } from './cloud-storage-adapter'\nimport type { CloudStorageOptions } from './types'\n\nexport { generateProjectKey, hashProjectKey } from './project-key'\nexport { CloudStorageAdapter } from './cloud-storage-adapter'\nexport type { CloudStorageOptions } from './types'\n\nexport function createCloudStorage(opts: CloudStorageOptions): StorageAdapter {\n return new CloudStorageAdapter(opts)\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-remarq/cloud",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Cloud storage adapter for web-remarq — Supabase-backed sync",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -0,0 +1,7 @@
1
+ -- 002_lifecycle.sql
2
+ -- Adds lifecycle column for full audit trail (core v0.7.0 feature).
3
+ -- Backward-compat: existing rows get '[]'::jsonb; migrateAnnotation in core
4
+ -- synthesizes a `created` event on load if array is empty.
5
+
6
+ alter table annotations
7
+ add column lifecycle jsonb not null default '[]'::jsonb;