@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 +16 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/sql/002_lifecycle.sql +7 -0
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
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAAkD;AA4BlD,SAAS,gBAAgB,KAAgC;AA5BzD;AA6BE,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA,IACb,aAAa,IAAI;AAAA,IACjB,OAAO,IAAI;AAAA,IACX,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI;AAAA,IACpB,WAAW,IAAI;AAAA,IACf,QAAQ,IAAI;AAAA,IACZ,YAAW,SAAI,cAAJ,YAAiB,CAAC;AAAA,EAC/B;AACF;AAEA,SAAS,gBAAgB,GAAmC;AAC1D,SAAO;AAAA,IACL,IAAI,EAAE;AAAA,IACN,OAAO,EAAE;AAAA,IACT,UAAU,EAAE;AAAA,IACZ,iBAAiB,EAAE;AAAA,IACnB,aAAa,EAAE;AAAA,IACf,SAAS,EAAE;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,cAAc,EAAE;AAAA,IAChB,WAAW,EAAE;AAAA,IACb,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,EACrC;AACF;AAEO,IAAM,sBAAN,MAAoD;AAAA,EAKzD,YAAY,MAA2B;AAJvC,SAAS,eAAe;AA1D1B;AA+DI,SAAK,WAAU,UAAK,YAAL,YAAgB;AAC/B,SAAK,aAAS,iCAAa,KAAK,aAAa,KAAK,iBAAiB;AAAA,MACjE,QAAQ;AAAA,QACN,SAAS,EAAE,wBAAwB,KAAK,WAAW;AAAA,MACrD;AAAA,MACA,MAAM,EAAE,gBAAgB,MAAM;AAAA,IAChC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAiC;AACrC,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,OAChC,KAAK,aAAa,EAClB,OAAO,GAAG,EACV,MAAM,gBAAgB,EAAE,WAAW,KAAK,CAAC;AAE5C,QAAI,OAAO;AACT,aAAO,KAAK,YAA6B,OAAO,EAAE,SAAS,GAAG,aAAa,CAAC,EAAE,CAAC;AAAA,IACjF;AAEA,UAAM,OAAQ,sBAAQ,CAAC;AACvB,WAAO,EAAE,SAAS,GAAG,aAAa,KAAK,IAAI,eAAe,EAAE;AAAA,EAC9D;AAAA,EAEA,MAAM,KAAK,YAAuC;AAChD,UAAM,MAAM,gBAAgB,UAAU;AACtC,UAAM,EAAE,MAAM,IAAI,MAAM,KAAK,OAC1B,KAAK,aAAa,EAClB,OAAO,KAAK,EAAE,YAAY,KAAK,CAAC;AACnC,QAAI,MAAO,MAAK,YAAkB,OAAO,MAAS;AAAA,EACpD;AAAA,EAEA,MAAM,OAAO,IAA2B;AACtC,UAAM,EAAE,MAAM,IAAI,MAAM,KAAK,OAAO,KAAK,aAAa,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE;AAC5E,QAAI,MAAO,MAAK,YAAkB,OAAO,MAAS;AAAA,EACpD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,EAAE,MAAM,IAAI,MAAM,KAAK,OAC1B,KAAK,aAAa,EAClB,OAAO,EACP,IAAI,MAAM,mBAAmB;AAChC,QAAI,MAAO,MAAK,YAAkB,OAAO,MAAS;AAAA,EACpD;AAAA,EAEQ,YAAe,OAAgB,UAAgB;AACrD,QAAI,KAAK,YAAY,SAAS;AAC5B,YAAM;AAAA,IACR;AACA,YAAQ,KAAK,sBAAsB,KAAK;AACxC,WAAO;AAAA,EACT;AACF;;;AClHA,IAAM,SAAS;AACf,IAAM,aAAa;AACnB,IAAM,WAAW;AAEV,SAAS,qBAA6B;AAC3C,QAAM,QAAQ,IAAI,WAAW,UAAU;AACvC,SAAO,gBAAgB,KAAK;AAC5B,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,WAAO,SAAS,MAAM,CAAC,IAAI,SAAS,MAAM;AAAA,EAC5C;AACA,SAAO;AACT;AAEA,eAAsB,eAAe,KAA8B;AACjE,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,GAAG;AACzC,QAAM,MAAM,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AACtD,SAAO,MAAM,KAAK,IAAI,WAAW,GAAG,CAAC,EAClC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAC1C,KAAK,EAAE;AACZ;;;AFZO,SAAS,mBAAmB,MAA2C;AAC5E,SAAO,IAAI,oBAAoB,IAAI;AACrC;","names":[]}
|
package/dist/index.js
CHANGED
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
|
|
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
|
@@ -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;
|