discovery-media-player 0.1.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/CONTRAT.md +515 -0
- package/LICENSE +661 -0
- package/LICENSE-MIT +21 -0
- package/README.md +152 -0
- package/bin/__tests__/serve.test.js +84 -0
- package/bin/serve.js +115 -0
- package/context/__tests__/storage.test.js +99 -0
- package/context/standalone.js +224 -0
- package/context/storage.js +230 -0
- package/package.json +72 -0
- package/server/brands.js +44 -0
- package/server/browser.generated.js +7 -0
- package/server/handler.js +2657 -0
- package/server/presentations.js +319 -0
- package/server/shared.generated.js +93 -0
- package/server/shares.js +275 -0
- package/src/__tests__/bridge.test.ts +108 -0
- package/src/__tests__/chat.test.ts +138 -0
- package/src/__tests__/live.test.ts +211 -0
- package/src/__tests__/presentation-content.test.ts +132 -0
- package/src/__tests__/presentation-state.test.ts +81 -0
- package/src/__tests__/tracking.test.ts +217 -0
- package/src/__tests__/viewer.test.ts +133 -0
- package/src/bridge.ts +141 -0
- package/src/chat.ts +103 -0
- package/src/index.ts +14 -0
- package/src/live.ts +225 -0
- package/src/presentation-content.ts +109 -0
- package/src/presentation-state.ts +93 -0
- package/src/tracking.ts +250 -0
- package/src/viewer.ts +109 -0
- package/supabase/init.sql +242 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
-- ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
-- DISCOVERY MEDIA PLAYER — schéma d'installation
|
|
3
|
+
--
|
|
4
|
+
-- Amène une base VIERGE à l'état attendu par le player. Un seul fichier, rejouable
|
|
5
|
+
-- (tout est `if not exists` / `create or replace`), sans rien à lire ailleurs.
|
|
6
|
+
--
|
|
7
|
+
-- ⚠️ POURQUOI UN INIT PLUTÔT QUE LA SUITE DES MIGRATIONS. Les tables du player sont nées
|
|
8
|
+
-- entrelacées dans les 145 migrations de son premier hôte. Reconstituer leur état en rejouant
|
|
9
|
+
-- cette suite obligerait chaque nouvel hôte à décider, fichier par fichier, lesquelles le
|
|
10
|
+
-- concernent — depuis un dépôt qu'il ne connaît pas, en devinant. Cette question a une bonne
|
|
11
|
+
-- réponse une seule fois : ici. (Demandée par le second hôte avant sa propre installation.)
|
|
12
|
+
--
|
|
13
|
+
-- ⚠️ CE FICHIER INSTALLE DÉJÀ DURCI. L'hôte historique a connu un état transitoire où les
|
|
14
|
+
-- présentations étaient lisibles anonymement (le suivi en direct passait par la lecture de
|
|
15
|
+
-- table, or Supabase Realtime `postgres_changes` exige un SELECT au niveau de la TABLE — donc
|
|
16
|
+
-- ouvert à quiconque a la clé publiable). Le player suit maintenant l'état par BROADCAST : une
|
|
17
|
+
-- base neuve n'a aucune raison de passer par là, et **ne doit pas**. Il n'y a rien à durcir
|
|
18
|
+
-- après coup, et donc aucune fenêtre pendant laquelle un document sensible serait exposé.
|
|
19
|
+
--
|
|
20
|
+
-- Une base EXISTANTE, elle, doit d'abord déployer le code broadcast et le vérifier avant de
|
|
21
|
+
-- retirer ces politiques : les retirer trop tôt fige les audiences en cours, sans erreur
|
|
22
|
+
-- visible. Cet avertissement ne concerne pas une installation neuve.
|
|
23
|
+
--
|
|
24
|
+
-- ACCÈS. Aucune politique RLS permissive n'est créée : RLS est activé et **rien ne passe** hors
|
|
25
|
+
-- `service_role`. C'est voulu — le player n'est jamais appelé depuis le navigateur avec la clé
|
|
26
|
+
-- publiable, il est appelé côté serveur. Toute lecture passe par sa route, qui décide.
|
|
27
|
+
-- ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
-- ── Liens de partage tracés ────────────────────────────────────────────────────────────────────
|
|
30
|
+
-- Un lien par destinataire : c'est ce qui permet de dire QUI a lu, pas seulement COMBIEN de fois.
|
|
31
|
+
create table if not exists public.commercial_doc_shares (
|
|
32
|
+
slug text primary key, -- randomBytes(9).toString("base64url")
|
|
33
|
+
doc_id text not null, -- identifiant du document CHEZ L'HÔTE (opaque ici)
|
|
34
|
+
doc_title text,
|
|
35
|
+
file_url text not null, -- doit passer la garde anti-SSRF du player
|
|
36
|
+
file_name text, -- ⚠️ dit la NATURE du document (pdf vs image)
|
|
37
|
+
recipient_email text,
|
|
38
|
+
recipient_name text,
|
|
39
|
+
created_by text,
|
|
40
|
+
created_at timestamptz not null default now(),
|
|
41
|
+
revoked boolean not null default false,
|
|
42
|
+
parent_slug text, -- re-partage : le lien d'où celui-ci est issu
|
|
43
|
+
bot_enabled boolean not null default false,
|
|
44
|
+
bot_script text,
|
|
45
|
+
bot_guided boolean not null default true,
|
|
46
|
+
bot_profile_id text,
|
|
47
|
+
allow_download boolean not null default true,
|
|
48
|
+
is_test boolean default false, -- répétition générale : exclue des statistiques
|
|
49
|
+
video_layout text,
|
|
50
|
+
brand_logo text, -- logo RECOPIÉ (flux historiques, toujours accepté)
|
|
51
|
+
brand_dark boolean not null default false,
|
|
52
|
+
require_auth boolean not null default false,
|
|
53
|
+
brand_key text -- ⚠️ RÉFÉRENCE résolue à l'affichage (branding.forKey)
|
|
54
|
+
);
|
|
55
|
+
create index if not exists cds_doc_id_idx on public.commercial_doc_shares (doc_id);
|
|
56
|
+
create index if not exists cds_parent_idx on public.commercial_doc_shares (parent_slug);
|
|
57
|
+
create index if not exists idx_doc_shares_brand_key
|
|
58
|
+
on public.commercial_doc_shares (brand_key) where brand_key is not null;
|
|
59
|
+
|
|
60
|
+
-- ── Journal des ouvertures (population EXTERNE) ────────────────────────────────────────────────
|
|
61
|
+
create table if not exists public.commercial_doc_views (
|
|
62
|
+
id uuid primary key default gen_random_uuid(),
|
|
63
|
+
slug text not null,
|
|
64
|
+
doc_id text,
|
|
65
|
+
recipient_email text,
|
|
66
|
+
event text not null, -- open | page | heartbeat
|
|
67
|
+
page integer,
|
|
68
|
+
max_page integer,
|
|
69
|
+
seconds integer,
|
|
70
|
+
session_id text,
|
|
71
|
+
at timestamptz not null default now(),
|
|
72
|
+
ua text
|
|
73
|
+
);
|
|
74
|
+
create index if not exists cdv_slug_idx on public.commercial_doc_views (slug);
|
|
75
|
+
create index if not exists cdv_doc_idx on public.commercial_doc_views (doc_id, at desc);
|
|
76
|
+
create index if not exists commercial_doc_views_at_idx on public.commercial_doc_views (at);
|
|
77
|
+
|
|
78
|
+
-- ── Sessions de lecture : le résumé riche (temps par page, appareil) ───────────────────────────
|
|
79
|
+
-- ⚠️ DEUX POPULATIONS, JAMAIS FUSIONNÉES. Un destinataire externe et un membre de l'hôte qui
|
|
80
|
+
-- relit son propre document ne racontent pas la même chose : mélangés, le second gonfle les
|
|
81
|
+
-- statistiques du premier et « ce prospect a lu 12 minutes » devient faux.
|
|
82
|
+
create table if not exists public.commercial_doc_sessions (
|
|
83
|
+
session_id text primary key,
|
|
84
|
+
slug text not null,
|
|
85
|
+
doc_id text,
|
|
86
|
+
recipient_email text,
|
|
87
|
+
num_pages integer,
|
|
88
|
+
max_page integer,
|
|
89
|
+
total_seconds integer default 0,
|
|
90
|
+
pages_time jsonb default '{}'::jsonb,
|
|
91
|
+
ua text,
|
|
92
|
+
ip text,
|
|
93
|
+
device text,
|
|
94
|
+
os text,
|
|
95
|
+
browser text,
|
|
96
|
+
started_at timestamptz not null default now(),
|
|
97
|
+
last_at timestamptz not null default now()
|
|
98
|
+
);
|
|
99
|
+
create index if not exists cds_sess_slug_idx on public.commercial_doc_sessions (slug);
|
|
100
|
+
create index if not exists cds_sess_doc_idx on public.commercial_doc_sessions (doc_id, last_at desc);
|
|
101
|
+
|
|
102
|
+
create table if not exists public.commercial_doc_internal_sessions (
|
|
103
|
+
session_id text primary key,
|
|
104
|
+
doc_id text,
|
|
105
|
+
user_email text,
|
|
106
|
+
user_name text,
|
|
107
|
+
num_pages integer,
|
|
108
|
+
max_page integer,
|
|
109
|
+
total_seconds integer default 0,
|
|
110
|
+
pages_time jsonb default '{}'::jsonb,
|
|
111
|
+
device text,
|
|
112
|
+
os text,
|
|
113
|
+
browser text,
|
|
114
|
+
started_at timestamptz not null default now(),
|
|
115
|
+
last_at timestamptz not null default now()
|
|
116
|
+
);
|
|
117
|
+
create index if not exists cdis_doc_idx on public.commercial_doc_internal_sessions (doc_id, last_at desc);
|
|
118
|
+
create index if not exists commercial_doc_internal_sessions_last_at_idx
|
|
119
|
+
on public.commercial_doc_internal_sessions (last_at);
|
|
120
|
+
|
|
121
|
+
-- ── Présentation en direct ─────────────────────────────────────────────────────────────────────
|
|
122
|
+
-- `control_hash` : c'est LUI qui distingue le présentateur de l'audience. Le slug seul ne donne
|
|
123
|
+
-- que le droit de suivre.
|
|
124
|
+
create table if not exists public.doc_presentations (
|
|
125
|
+
slug text primary key,
|
|
126
|
+
control_hash text not null,
|
|
127
|
+
doc_id text,
|
|
128
|
+
file_url text not null,
|
|
129
|
+
file_name text,
|
|
130
|
+
doc_title text,
|
|
131
|
+
presenter_name text,
|
|
132
|
+
current_page integer not null default 1,
|
|
133
|
+
active boolean not null default true,
|
|
134
|
+
created_at timestamptz not null default now(),
|
|
135
|
+
updated_at timestamptz not null default now(),
|
|
136
|
+
chat_locked boolean not null default false,
|
|
137
|
+
owner_user_id text,
|
|
138
|
+
owner_name text,
|
|
139
|
+
owner_avatar text,
|
|
140
|
+
owner_email text,
|
|
141
|
+
last_seen timestamptz not null default now(),
|
|
142
|
+
content jsonb -- carte / Street View (revalidé à la réception)
|
|
143
|
+
);
|
|
144
|
+
create index if not exists doc_presentations_active_idx on public.doc_presentations (active, updated_at);
|
|
145
|
+
create index if not exists doc_presentations_last_seen_idx on public.doc_presentations (active, last_seen);
|
|
146
|
+
create index if not exists doc_presentations_owner_active_idx on public.doc_presentations (owner_user_id, active);
|
|
147
|
+
create index if not exists doc_presentations_owner_email_active_idx on public.doc_presentations (owner_email, active);
|
|
148
|
+
|
|
149
|
+
create table if not exists public.doc_presentation_messages (
|
|
150
|
+
id bigint generated by default as identity primary key,
|
|
151
|
+
slug text not null,
|
|
152
|
+
author_name text,
|
|
153
|
+
author_email text,
|
|
154
|
+
author_avatar text,
|
|
155
|
+
author_hash text, -- ⚠️ jeton d'auteur : autorise l'édition/suppression
|
|
156
|
+
is_presenter boolean not null default false,
|
|
157
|
+
is_member boolean not null default false,
|
|
158
|
+
body text not null,
|
|
159
|
+
created_at timestamptz not null default now(),
|
|
160
|
+
reactions jsonb not null default '{}'::jsonb,
|
|
161
|
+
reply_to bigint,
|
|
162
|
+
reply_name text,
|
|
163
|
+
reply_text text,
|
|
164
|
+
deleted boolean not null default false,
|
|
165
|
+
edited boolean not null default false,
|
|
166
|
+
attachment jsonb
|
|
167
|
+
);
|
|
168
|
+
create index if not exists dpm_slug_idx on public.doc_presentation_messages (slug, created_at);
|
|
169
|
+
|
|
170
|
+
create table if not exists public.doc_presentation_attendees (
|
|
171
|
+
slug text not null,
|
|
172
|
+
attendee_key text not null,
|
|
173
|
+
name text,
|
|
174
|
+
email text,
|
|
175
|
+
avatar text,
|
|
176
|
+
is_member boolean not null default false,
|
|
177
|
+
is_presenter boolean not null default false,
|
|
178
|
+
first_seen timestamptz not null default now(),
|
|
179
|
+
last_seen timestamptz not null default now(),
|
|
180
|
+
total_ms bigint not null default 0,
|
|
181
|
+
pages jsonb not null default '[]'::jsonb,
|
|
182
|
+
primary key (slug, attendee_key)
|
|
183
|
+
);
|
|
184
|
+
create index if not exists doc_presentation_attendees_slug_idx on public.doc_presentation_attendees (slug);
|
|
185
|
+
|
|
186
|
+
-- ── Assistant IA (greffon `bot`) ───────────────────────────────────────────────────────────────
|
|
187
|
+
-- Facultatif : le player fonctionne sans (PLAYER_PLUGINS_OFF=bot). La table peut rester vide.
|
|
188
|
+
create table if not exists public.doc_bot_sessions (
|
|
189
|
+
id text primary key,
|
|
190
|
+
share_slug text not null,
|
|
191
|
+
doc_id text,
|
|
192
|
+
current_page integer not null default 1,
|
|
193
|
+
phase text not null default 'intro',
|
|
194
|
+
created_at timestamptz not null default now(),
|
|
195
|
+
last_at timestamptz not null default now(),
|
|
196
|
+
in_tokens bigint not null default 0,
|
|
197
|
+
cache_tokens bigint not null default 0,
|
|
198
|
+
out_tokens bigint not null default 0,
|
|
199
|
+
ai_calls integer not null default 0,
|
|
200
|
+
ai_cost numeric not null default 0,
|
|
201
|
+
bot_profile_id text,
|
|
202
|
+
mobile boolean,
|
|
203
|
+
is_test boolean default false,
|
|
204
|
+
rating smallint,
|
|
205
|
+
rating_comment text,
|
|
206
|
+
agent_id text,
|
|
207
|
+
journey_step text,
|
|
208
|
+
etat jsonb
|
|
209
|
+
);
|
|
210
|
+
create index if not exists doc_bot_sessions_share_idx on public.doc_bot_sessions (share_slug);
|
|
211
|
+
|
|
212
|
+
-- ── Accès ──────────────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
-- RLS activé SANS politique permissive : seul `service_role` (donc la route du player) passe.
|
|
214
|
+
-- ⚠️ N'ajoutez pas de politique de lecture publique « pour que le direct fonctionne ». C'est
|
|
215
|
+
-- précisément l'état dont l'hôte historique a dû sortir : le suivi passe par broadcast.
|
|
216
|
+
alter table public.commercial_doc_shares enable row level security;
|
|
217
|
+
alter table public.commercial_doc_views enable row level security;
|
|
218
|
+
alter table public.commercial_doc_sessions enable row level security;
|
|
219
|
+
alter table public.commercial_doc_internal_sessions enable row level security;
|
|
220
|
+
alter table public.doc_presentations enable row level security;
|
|
221
|
+
alter table public.doc_presentation_messages enable row level security;
|
|
222
|
+
alter table public.doc_presentation_attendees enable row level security;
|
|
223
|
+
alter table public.doc_bot_sessions enable row level security;
|
|
224
|
+
|
|
225
|
+
-- ── Temps réel ─────────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
-- Le chat en direct reste en `postgres_changes` : ses messages sont écrits POUR être vus de
|
|
227
|
+
-- l'audience. L'état de la présentation, lui, voyage en broadcast — d'où l'absence de
|
|
228
|
+
-- `doc_presentations` ici, et l'absence de politique de lecture au-dessus.
|
|
229
|
+
-- ⚠️ Ne mettez jamais de donnée confidentielle dans un message de chat ni dans un nom d'auteur.
|
|
230
|
+
do $$
|
|
231
|
+
begin
|
|
232
|
+
if exists (select 1 from pg_publication where pubname = 'supabase_realtime') then
|
|
233
|
+
if not exists (
|
|
234
|
+
select 1 from pg_publication_tables
|
|
235
|
+
where pubname = 'supabase_realtime' and schemaname = 'public'
|
|
236
|
+
and tablename = 'doc_presentation_messages'
|
|
237
|
+
) then
|
|
238
|
+
execute 'alter publication supabase_realtime add table public.doc_presentation_messages';
|
|
239
|
+
end if;
|
|
240
|
+
end if;
|
|
241
|
+
end $$;
|
|
242
|
+
alter table public.doc_presentation_messages replica identity full;
|