@gamaro.io/core-types 0.6.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/LICENSE +48 -0
- package/README.md +104 -0
- package/dist/application-config.d.ts +64 -0
- package/dist/application-config.js +45 -0
- package/dist/geo.d.ts +24 -0
- package/dist/geo.js +25 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/motion.d.ts +56 -0
- package/dist/motion.js +54 -0
- package/dist/postmessage.d.ts +162 -0
- package/dist/postmessage.js +91 -0
- package/dist/session.d.ts +25 -0
- package/dist/session.js +32 -0
- package/dist/theme-resolver.d.ts +54 -0
- package/dist/theme-resolver.js +40 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +39 -0
- package/dist/viewport.d.ts +32 -0
- package/dist/viewport.js +36 -0
- package/dist/widget-config.d.ts +44 -0
- package/dist/widget-config.js +62 -0
- package/dist/widget-runtime-config.d.ts +48 -0
- package/dist/widget-runtime-config.js +63 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Versiq Core Types — Commercial Source-available License
|
|
2
|
+
========================================================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024-2026 Sébastien Dolard
|
|
5
|
+
|
|
6
|
+
This package ships TypeScript type contracts and Zod schemas consumed by the
|
|
7
|
+
Versiq Widget SDK. It is published under the same license as the widget
|
|
8
|
+
bundle so that TypeScript integrators get a working type tree when they
|
|
9
|
+
install `@gamaro.io/widget`.
|
|
10
|
+
|
|
11
|
+
1. Permitted use
|
|
12
|
+
You may, without separate written agreement:
|
|
13
|
+
(a) Install this package as a transitive dependency of
|
|
14
|
+
`@gamaro.io/widget` via standard package managers.
|
|
15
|
+
(b) Import the exported types and Zod schemas in client-side or
|
|
16
|
+
server-side code that interoperates with the Versiq backend API.
|
|
17
|
+
|
|
18
|
+
2. Prohibited without prior written agreement
|
|
19
|
+
You may not, in whole or in part:
|
|
20
|
+
(a) Modify, fork, or create derivative works for redistribution under
|
|
21
|
+
any name.
|
|
22
|
+
(b) Redistribute the package, or any derivative, under a different
|
|
23
|
+
package name, namespace, or scope.
|
|
24
|
+
(c) Use these contracts to design or implement a competing widget SDK,
|
|
25
|
+
qualification backend, or conversation orchestration product.
|
|
26
|
+
(d) Remove, alter, or obscure copyright notices, attribution, or this
|
|
27
|
+
license file from any copy.
|
|
28
|
+
|
|
29
|
+
3. Production use
|
|
30
|
+
Use of the corresponding Versiq backend in production is subject to the
|
|
31
|
+
Versiq Commercial Terms of Service. The license granted here is for the
|
|
32
|
+
type contracts only; it does not authorize backend access.
|
|
33
|
+
|
|
34
|
+
4. No warranty
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
36
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
37
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
38
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
39
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
40
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
41
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
42
|
+
|
|
43
|
+
5. Termination
|
|
44
|
+
Any breach of section 2 terminates your rights under section 1
|
|
45
|
+
immediately, without notice, and you must cease all use of the software
|
|
46
|
+
and destroy all copies in your possession or control.
|
|
47
|
+
|
|
48
|
+
For commercial licensing or written-agreement inquiries: admin@dolard.eu
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @gamaro.io/core-types
|
|
2
|
+
|
|
3
|
+
Cross-workspace TypeScript contracts and Zod schemas consumed by
|
|
4
|
+
[`@gamaro.io/widget`](https://www.npmjs.com/package/@gamaro.io/widget).
|
|
5
|
+
|
|
6
|
+
> **Companion package — not for standalone use.** This package exists to share
|
|
7
|
+
> type definitions between the Versiq backend, the marketing site, and the
|
|
8
|
+
> public widget SDK. If you are integrating the widget into your site, you need
|
|
9
|
+
> [`@gamaro.io/widget`](https://www.npmjs.com/package/@gamaro.io/widget), not
|
|
10
|
+
> this one. The widget already re-exports the types you need.
|
|
11
|
+
|
|
12
|
+
## When you might need this directly
|
|
13
|
+
|
|
14
|
+
You only need to depend on `@gamaro.io/core-types` if you are building a custom
|
|
15
|
+
integration that talks to the Versiq backend without going through the widget
|
|
16
|
+
SDK — for example, a server-to-server pipeline that posts events to the Versiq
|
|
17
|
+
API and wants the same Zod schemas the official widget uses.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @gamaro.io/core-types zod
|
|
23
|
+
# or
|
|
24
|
+
pnpm add @gamaro.io/core-types zod
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`zod` is a peer dependency: schemas are runtime-validatable.
|
|
28
|
+
|
|
29
|
+
## Scope (ADR-006)
|
|
30
|
+
|
|
31
|
+
The package contract is intentionally narrow: only types and schemas that cross
|
|
32
|
+
a workspace boundary live here. Domain-specific schemas (real-estate profiles,
|
|
33
|
+
B2B qualification, scoring rules, …) stay in the workspace that owns the domain.
|
|
34
|
+
|
|
35
|
+
## Public exports
|
|
36
|
+
|
|
37
|
+
| Module | Contents |
|
|
38
|
+
| ----------------------- | ------------------------------------------------------------------------ |
|
|
39
|
+
| `theme` | `ThemeConfig`, hex-color constraints, default palette |
|
|
40
|
+
| `theme-resolver` | Merges host-side theme with server-side theme; returns the final config |
|
|
41
|
+
| `viewport` | Viewport bucket helpers (`mobile` / `tablet` / `desktop`) + breakpoints |
|
|
42
|
+
| `widget-config` | `WidgetConfig` — the public contract of `createWidget(config)` |
|
|
43
|
+
| `widget-runtime-config` | Internal runtime view of the resolved widget config (after server merge) |
|
|
44
|
+
| `application-config` | Server-side Agent metadata (vertical, allowed origins, identity) |
|
|
45
|
+
| `postmessage` | Discriminated unions for `window.postMessage` host ↔ iframe protocol |
|
|
46
|
+
| `geo` | Lightweight geo primitives (`LatLng`, `BoundingBox`) |
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import type {
|
|
50
|
+
ThemeConfig,
|
|
51
|
+
WidgetConfig,
|
|
52
|
+
ViewportBucket,
|
|
53
|
+
HostToWidgetMessage,
|
|
54
|
+
WidgetToHostMessage,
|
|
55
|
+
} from "@gamaro.io/core-types";
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
themeConfigSchema,
|
|
59
|
+
hostToWidgetMessageSchema,
|
|
60
|
+
} from "@gamaro.io/core-types";
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Subpath-only modules
|
|
64
|
+
|
|
65
|
+
Some helpers are exposed on a **dedicated subpath**, deliberately kept out of
|
|
66
|
+
the barrel above so they carry no `zod` dependency (import them directly, not
|
|
67
|
+
from the package root):
|
|
68
|
+
|
|
69
|
+
| Subpath | Contents |
|
|
70
|
+
| ---------- | ----------------------------------------------------------------------------- |
|
|
71
|
+
| `/session` | `getSessionCookieName`, `isValidSessionId` — pure session cookie / id helpers |
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import {
|
|
75
|
+
getSessionCookieName,
|
|
76
|
+
isValidSessionId,
|
|
77
|
+
} from "@gamaro.io/core-types/session";
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
All schemas are Zod 4 (`zod@^4.2.0`) and infer their TypeScript types via
|
|
81
|
+
`z.infer<typeof xxxSchema>` — the same `.parse()` / `.safeParse()` API as the
|
|
82
|
+
widget itself.
|
|
83
|
+
|
|
84
|
+
## Versioning
|
|
85
|
+
|
|
86
|
+
Released in lockstep with `@gamaro.io/widget` (same version number). Breaking
|
|
87
|
+
changes follow SemVer:
|
|
88
|
+
|
|
89
|
+
- **MINOR** — added optional fields, added discriminant values, new schemas
|
|
90
|
+
- **MAJOR** — removed/renamed fields, narrowed unions, schema-level breaking
|
|
91
|
+
changes
|
|
92
|
+
|
|
93
|
+
The widget SDK pins its dependency on `@gamaro.io/core-types` to the exact same
|
|
94
|
+
version it ships with — you never need to align them yourself.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
Commercial Source-available — see [LICENSE](./LICENSE).
|
|
99
|
+
|
|
100
|
+
This is **not** an open-source release. The source is published so integrators
|
|
101
|
+
can audit what runs in their visitors' browsers, but fork / redistribution /
|
|
102
|
+
competing-SDK use are prohibited without a written agreement.
|
|
103
|
+
|
|
104
|
+
For commercial licensing inquiries: admin@dolard.eu
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application bootstrap config returned by `GET /api/application/config`.
|
|
3
|
+
*
|
|
4
|
+
* The app (`apps/app`) produces this response through a stricter server-side
|
|
5
|
+
* whitelist schema (`publicApplicationConfigSchema` in the route), and the
|
|
6
|
+
* widget (`@gamaro.io/widget`) consumes it here. Keeping the widget-side Zod
|
|
7
|
+
* schema as the canonical shape prevents drift — the server schema MUST remain
|
|
8
|
+
* a compatible subset (it may be stricter, never more permissive).
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
export declare const applicationConfigResponseSchema: z.ZodObject<{
|
|
12
|
+
applicationId: z.ZodString;
|
|
13
|
+
applicationSlug: z.ZodString;
|
|
14
|
+
applicationName: z.ZodString;
|
|
15
|
+
vertical: z.ZodString;
|
|
16
|
+
theme: z.ZodNullable<z.ZodObject<{
|
|
17
|
+
primaryColor: z.ZodOptional<z.ZodString>;
|
|
18
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
19
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
20
|
+
borderRadius: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
fontFamily: z.ZodOptional<z.ZodString>;
|
|
22
|
+
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
light: "light";
|
|
24
|
+
dark: "dark";
|
|
25
|
+
auto: "auto";
|
|
26
|
+
}>>;
|
|
27
|
+
}, z.core.$strip>>;
|
|
28
|
+
allowedOrigins: z.ZodArray<z.ZodString>;
|
|
29
|
+
monitoring: z.ZodBoolean;
|
|
30
|
+
primaryObjective: z.ZodNullable<z.ZodObject<{
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
type: z.ZodString;
|
|
33
|
+
label: z.ZodString;
|
|
34
|
+
ctaLabel: z.ZodNullable<z.ZodString>;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
identityVerificationRequired: z.ZodBoolean;
|
|
37
|
+
widget: z.ZodNullable<z.ZodObject<{
|
|
38
|
+
theme: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
primaryColor: z.ZodOptional<z.ZodString>;
|
|
40
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
41
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
42
|
+
borderRadius: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
fontFamily: z.ZodOptional<z.ZodString>;
|
|
44
|
+
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
45
|
+
light: "light";
|
|
46
|
+
dark: "dark";
|
|
47
|
+
auto: "auto";
|
|
48
|
+
}>>;
|
|
49
|
+
}, z.core.$strip>>;
|
|
50
|
+
position: z.ZodOptional<z.ZodEnum<{
|
|
51
|
+
"bottom-right": "bottom-right";
|
|
52
|
+
"bottom-left": "bottom-left";
|
|
53
|
+
inline: "inline";
|
|
54
|
+
}>>;
|
|
55
|
+
language: z.ZodOptional<z.ZodString>;
|
|
56
|
+
showProfile: z.ZodOptional<z.ZodBoolean>;
|
|
57
|
+
open: z.ZodOptional<z.ZodBoolean>;
|
|
58
|
+
brand: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
title: z.ZodOptional<z.ZodString>;
|
|
60
|
+
avatarUrl: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, z.core.$strict>>;
|
|
62
|
+
}, z.core.$strict>>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
export type ApplicationConfigResponse = z.infer<typeof applicationConfigResponseSchema>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application bootstrap config returned by `GET /api/application/config`.
|
|
3
|
+
*
|
|
4
|
+
* The app (`apps/app`) produces this response through a stricter server-side
|
|
5
|
+
* whitelist schema (`publicApplicationConfigSchema` in the route), and the
|
|
6
|
+
* widget (`@gamaro.io/widget`) consumes it here. Keeping the widget-side Zod
|
|
7
|
+
* schema as the canonical shape prevents drift — the server schema MUST remain
|
|
8
|
+
* a compatible subset (it may be stricter, never more permissive).
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { themeConfigSchema } from "./theme";
|
|
12
|
+
import { widgetRuntimeConfigSchema } from "./widget-runtime-config";
|
|
13
|
+
export const applicationConfigResponseSchema = z.object({
|
|
14
|
+
applicationId: z.string().uuid().describe("Application UUID"),
|
|
15
|
+
applicationSlug: z
|
|
16
|
+
.string()
|
|
17
|
+
.max(100)
|
|
18
|
+
.describe("Application slug (kebab-case)"),
|
|
19
|
+
applicationName: z.string().max(200).describe("Application display name"),
|
|
20
|
+
vertical: z
|
|
21
|
+
.string()
|
|
22
|
+
.max(50)
|
|
23
|
+
.describe("Vertical resolved from Application config"),
|
|
24
|
+
theme: themeConfigSchema.nullable().describe("Application theme"),
|
|
25
|
+
allowedOrigins: z
|
|
26
|
+
.array(z.string().max(200))
|
|
27
|
+
.max(50)
|
|
28
|
+
.describe("Allowed origins for this application"),
|
|
29
|
+
monitoring: z.boolean().describe("Monitoring enabled flag"),
|
|
30
|
+
primaryObjective: z
|
|
31
|
+
.object({
|
|
32
|
+
id: z.string().uuid(),
|
|
33
|
+
type: z.string(),
|
|
34
|
+
label: z.string(),
|
|
35
|
+
ctaLabel: z.string().nullable(),
|
|
36
|
+
})
|
|
37
|
+
.nullable()
|
|
38
|
+
.describe("Primary conversion objective"),
|
|
39
|
+
identityVerificationRequired: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.describe("Whether HMAC identity verification is required"),
|
|
42
|
+
widget: widgetRuntimeConfigSchema
|
|
43
|
+
.nullable()
|
|
44
|
+
.describe("Admin-configured widget runtime config (theme, position, language, showProfile, open). Null when the application has not yet persisted a widget_config JSONB."),
|
|
45
|
+
});
|
package/dist/geo.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geographic cross-boundary type contracts.
|
|
3
|
+
*
|
|
4
|
+
* Cross-boundary contract shared by the geo package (`@versiq/geo`) and the
|
|
5
|
+
* app host (`apps/app`). Previously co-located in `apps/app/src/lib/ai/schemas/`
|
|
6
|
+
* by historical happenstance — directions cardinales are not AI-specific, they
|
|
7
|
+
* are pure geographic primitives.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
/**
|
|
11
|
+
* Cardinal directions for directional zone search (e.g., "nord de Lyon").
|
|
12
|
+
* Uses English keys for LLM consistency, mapped to French for display.
|
|
13
|
+
*/
|
|
14
|
+
export declare const cardinalDirectionEnum: z.ZodEnum<{
|
|
15
|
+
north: "north";
|
|
16
|
+
northeast: "northeast";
|
|
17
|
+
east: "east";
|
|
18
|
+
southeast: "southeast";
|
|
19
|
+
south: "south";
|
|
20
|
+
southwest: "southwest";
|
|
21
|
+
west: "west";
|
|
22
|
+
northwest: "northwest";
|
|
23
|
+
}>;
|
|
24
|
+
export type CardinalDirection = z.infer<typeof cardinalDirectionEnum>;
|
package/dist/geo.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geographic cross-boundary type contracts.
|
|
3
|
+
*
|
|
4
|
+
* Cross-boundary contract shared by the geo package (`@versiq/geo`) and the
|
|
5
|
+
* app host (`apps/app`). Previously co-located in `apps/app/src/lib/ai/schemas/`
|
|
6
|
+
* by historical happenstance — directions cardinales are not AI-specific, they
|
|
7
|
+
* are pure geographic primitives.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
/**
|
|
11
|
+
* Cardinal directions for directional zone search (e.g., "nord de Lyon").
|
|
12
|
+
* Uses English keys for LLM consistency, mapped to French for display.
|
|
13
|
+
*/
|
|
14
|
+
export const cardinalDirectionEnum = z
|
|
15
|
+
.enum([
|
|
16
|
+
"north",
|
|
17
|
+
"northeast",
|
|
18
|
+
"east",
|
|
19
|
+
"southeast",
|
|
20
|
+
"south",
|
|
21
|
+
"southwest",
|
|
22
|
+
"west",
|
|
23
|
+
"northwest",
|
|
24
|
+
])
|
|
25
|
+
.describe("Direction cardinale (north/south/east/west + combinaisons)");
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gamaro.io/core-types
|
|
3
|
+
*
|
|
4
|
+
* Cross-workspace type contracts shared by `apps/app`, `apps/marketing` and
|
|
5
|
+
* `packages/widget`. Scope is intentionally narrow (ADR-006): only types that
|
|
6
|
+
* cross a workspace boundary live here. Do NOT dump domain-specific schemas
|
|
7
|
+
* in this package — keep them in the consumer that owns the domain.
|
|
8
|
+
*
|
|
9
|
+
* See `docs/adr/ADR-006-monorepo-package-strategy.md`.
|
|
10
|
+
*/
|
|
11
|
+
export * from "./motion";
|
|
12
|
+
export * from "./theme";
|
|
13
|
+
export * from "./theme-resolver";
|
|
14
|
+
export * from "./viewport";
|
|
15
|
+
export * from "./widget-config";
|
|
16
|
+
export * from "./widget-runtime-config";
|
|
17
|
+
export * from "./application-config";
|
|
18
|
+
export * from "./postmessage";
|
|
19
|
+
export * from "./geo";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gamaro.io/core-types
|
|
3
|
+
*
|
|
4
|
+
* Cross-workspace type contracts shared by `apps/app`, `apps/marketing` and
|
|
5
|
+
* `packages/widget`. Scope is intentionally narrow (ADR-006): only types that
|
|
6
|
+
* cross a workspace boundary live here. Do NOT dump domain-specific schemas
|
|
7
|
+
* in this package — keep them in the consumer that owns the domain.
|
|
8
|
+
*
|
|
9
|
+
* See `docs/adr/ADR-006-monorepo-package-strategy.md`.
|
|
10
|
+
*/
|
|
11
|
+
export * from "./motion";
|
|
12
|
+
export * from "./theme";
|
|
13
|
+
export * from "./theme-resolver";
|
|
14
|
+
export * from "./viewport";
|
|
15
|
+
export * from "./widget-config";
|
|
16
|
+
export * from "./widget-runtime-config";
|
|
17
|
+
export * from "./application-config";
|
|
18
|
+
export * from "./postmessage";
|
|
19
|
+
export * from "./geo";
|
package/dist/motion.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical motion tokens for the Versiq widget — the single source of truth
|
|
3
|
+
* for animation durations, easing curves and stagger timing across BOTH widget
|
|
4
|
+
* bundles (the vanilla loader in `packages/widget` and the React embed in
|
|
5
|
+
* `apps/app/src/app/widget/embed`).
|
|
6
|
+
*
|
|
7
|
+
* **This module MUST stay free of any import** (no Zod, no sibling types). It is
|
|
8
|
+
* consumed by the loader through the dedicated subpath export
|
|
9
|
+
* `@gamaro.io/core-types/motion`, which guarantees the loader's UMD
|
|
10
|
+
* bundle never pulls in Zod via the barrel `index.ts`. Adding an import here
|
|
11
|
+
* would break that guarantee.
|
|
12
|
+
*
|
|
13
|
+
* Design rationale (see `docs/adr/ADR-018-widget-motion-design.md`):
|
|
14
|
+
* - Durations follow a harmonic scale; all UI transitions stay <= 400ms so the
|
|
15
|
+
* interaction is perceived as instant (Doherty threshold, IBM 1982).
|
|
16
|
+
* - Easing curves are Material-3-style: entering elements decelerate (`enter`),
|
|
17
|
+
* leaving elements accelerate (`exit`), in-place motion uses `standard`. The
|
|
18
|
+
* "soft" feel comes from deceleration at the end of travel — never `linear`
|
|
19
|
+
* nor a steep `ease-in-out` for UI transitions.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Harmonic duration scale in milliseconds.
|
|
23
|
+
* - `instant` — no animation (reduced-motion fallback, snap states).
|
|
24
|
+
* - `fast` — micro-interactions (hover, focus).
|
|
25
|
+
* - `base` — standard transitions (fades, bubble entrances).
|
|
26
|
+
* - `slow` — panels, container resize, collapses.
|
|
27
|
+
* - `slower` — looping loaders (bounce/typing dots).
|
|
28
|
+
*/
|
|
29
|
+
export declare const MOTION_DURATIONS: {
|
|
30
|
+
readonly instant: 0;
|
|
31
|
+
readonly fast: 150;
|
|
32
|
+
readonly base: 250;
|
|
33
|
+
readonly slow: 400;
|
|
34
|
+
readonly slower: 600;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Named easing curves. `standard` for in-place motion, `enter` for elements
|
|
38
|
+
* arriving (decelerate), `exit` for elements leaving (accelerate), `emphasized`
|
|
39
|
+
* for prominent panel transitions.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MOTION_EASINGS: {
|
|
42
|
+
readonly standard: "cubic-bezier(0.4, 0, 0.2, 1)";
|
|
43
|
+
readonly enter: "cubic-bezier(0, 0, 0.2, 1)";
|
|
44
|
+
readonly exit: "cubic-bezier(0.4, 0, 1, 1)";
|
|
45
|
+
readonly emphasized: "cubic-bezier(0.2, 0, 0, 1)";
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Stagger timing for choreographed list entrances (quick replies, initial
|
|
49
|
+
* message batch). `step` is the per-item delay increment in milliseconds —
|
|
50
|
+
* a small cascade guides the eye and reads as a group (Gestalt common fate).
|
|
51
|
+
*/
|
|
52
|
+
export declare const MOTION_STAGGER: {
|
|
53
|
+
readonly step: 40;
|
|
54
|
+
};
|
|
55
|
+
export type MotionDurationKey = keyof typeof MOTION_DURATIONS;
|
|
56
|
+
export type MotionEasingKey = keyof typeof MOTION_EASINGS;
|
package/dist/motion.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical motion tokens for the Versiq widget — the single source of truth
|
|
3
|
+
* for animation durations, easing curves and stagger timing across BOTH widget
|
|
4
|
+
* bundles (the vanilla loader in `packages/widget` and the React embed in
|
|
5
|
+
* `apps/app/src/app/widget/embed`).
|
|
6
|
+
*
|
|
7
|
+
* **This module MUST stay free of any import** (no Zod, no sibling types). It is
|
|
8
|
+
* consumed by the loader through the dedicated subpath export
|
|
9
|
+
* `@gamaro.io/core-types/motion`, which guarantees the loader's UMD
|
|
10
|
+
* bundle never pulls in Zod via the barrel `index.ts`. Adding an import here
|
|
11
|
+
* would break that guarantee.
|
|
12
|
+
*
|
|
13
|
+
* Design rationale (see `docs/adr/ADR-018-widget-motion-design.md`):
|
|
14
|
+
* - Durations follow a harmonic scale; all UI transitions stay <= 400ms so the
|
|
15
|
+
* interaction is perceived as instant (Doherty threshold, IBM 1982).
|
|
16
|
+
* - Easing curves are Material-3-style: entering elements decelerate (`enter`),
|
|
17
|
+
* leaving elements accelerate (`exit`), in-place motion uses `standard`. The
|
|
18
|
+
* "soft" feel comes from deceleration at the end of travel — never `linear`
|
|
19
|
+
* nor a steep `ease-in-out` for UI transitions.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Harmonic duration scale in milliseconds.
|
|
23
|
+
* - `instant` — no animation (reduced-motion fallback, snap states).
|
|
24
|
+
* - `fast` — micro-interactions (hover, focus).
|
|
25
|
+
* - `base` — standard transitions (fades, bubble entrances).
|
|
26
|
+
* - `slow` — panels, container resize, collapses.
|
|
27
|
+
* - `slower` — looping loaders (bounce/typing dots).
|
|
28
|
+
*/
|
|
29
|
+
export const MOTION_DURATIONS = {
|
|
30
|
+
instant: 0,
|
|
31
|
+
fast: 150,
|
|
32
|
+
base: 250,
|
|
33
|
+
slow: 400,
|
|
34
|
+
slower: 600,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Named easing curves. `standard` for in-place motion, `enter` for elements
|
|
38
|
+
* arriving (decelerate), `exit` for elements leaving (accelerate), `emphasized`
|
|
39
|
+
* for prominent panel transitions.
|
|
40
|
+
*/
|
|
41
|
+
export const MOTION_EASINGS = {
|
|
42
|
+
standard: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
43
|
+
enter: "cubic-bezier(0, 0, 0.2, 1)",
|
|
44
|
+
exit: "cubic-bezier(0.4, 0, 1, 1)",
|
|
45
|
+
emphasized: "cubic-bezier(0.2, 0, 0, 1)",
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Stagger timing for choreographed list entrances (quick replies, initial
|
|
49
|
+
* message batch). `step` is the per-item delay increment in milliseconds —
|
|
50
|
+
* a small cascade guides the eye and reads as a group (Gestalt common fate).
|
|
51
|
+
*/
|
|
52
|
+
export const MOTION_STAGGER = {
|
|
53
|
+
step: 40,
|
|
54
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postMessage protocol between the widget iframe and its host page.
|
|
3
|
+
*
|
|
4
|
+
* Two directions share the `versiq:*` prefix:
|
|
5
|
+
* - `WidgetCommand` — parent page → iframe (commands)
|
|
6
|
+
* - `WidgetEvent` — iframe → parent page (events)
|
|
7
|
+
*
|
|
8
|
+
* The `MESSAGE_TYPES` const aggregates every message type string across both
|
|
9
|
+
* directions. Consumers that treat the protocol opaquely (e.g. origin
|
|
10
|
+
* validation helpers) can rely on it to check `type.startsWith("versiq:")`
|
|
11
|
+
* without enumerating every member.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { themeConfigSchema, type ThemeConfig } from "./theme";
|
|
15
|
+
import { viewportModeSchema, type ViewportMode } from "./viewport";
|
|
16
|
+
import { widgetConfigSchema, type WidgetConfig } from "./widget-config";
|
|
17
|
+
export declare const messageRoleSchema: z.ZodEnum<{
|
|
18
|
+
user: "user";
|
|
19
|
+
assistant: "assistant";
|
|
20
|
+
}>;
|
|
21
|
+
export type MessageRole = z.infer<typeof messageRoleSchema>;
|
|
22
|
+
export declare const widgetMessageSchema: z.ZodObject<{
|
|
23
|
+
role: z.ZodEnum<{
|
|
24
|
+
user: "user";
|
|
25
|
+
assistant: "assistant";
|
|
26
|
+
}>;
|
|
27
|
+
content: z.ZodString;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type WidgetMessage = z.infer<typeof widgetMessageSchema>;
|
|
30
|
+
/**
|
|
31
|
+
* Generic widget profile. The concrete shape depends on the vertical
|
|
32
|
+
* (`BuyerProfile` for real-estate, `B2BProfile` for b2b-qualification, etc.)
|
|
33
|
+
* — kept structural so verticals can evolve independently.
|
|
34
|
+
*
|
|
35
|
+
* As of #1993, no `WidgetEvent` member carries this type across the
|
|
36
|
+
* postMessage boundary anymore (see the signal-only `PROFILE_UPDATE`/
|
|
37
|
+
* `QUALIFIED` members below) — it is used for the widget's own internal
|
|
38
|
+
* conversation state and re-exported for integrators who want to type their
|
|
39
|
+
* own mirrored state.
|
|
40
|
+
*/
|
|
41
|
+
export type WidgetProfile = Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Exhaustive catalogue of every `versiq:*` message type used in either
|
|
44
|
+
* direction. Kept as a single const so a new event or command is declared in
|
|
45
|
+
* one place.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MESSAGE_TYPES: {
|
|
48
|
+
readonly INIT: "versiq:init";
|
|
49
|
+
readonly OPEN: "versiq:open";
|
|
50
|
+
readonly CLOSE: "versiq:close";
|
|
51
|
+
readonly RESET: "versiq:reset";
|
|
52
|
+
readonly SET_THEME: "versiq:setTheme";
|
|
53
|
+
readonly VIEWPORT: "versiq:viewport";
|
|
54
|
+
readonly IDENTIFY: "versiq:identify";
|
|
55
|
+
readonly READY: "versiq:ready";
|
|
56
|
+
readonly PROFILE_UPDATE: "versiq:profile-update";
|
|
57
|
+
readonly MESSAGE: "versiq:message";
|
|
58
|
+
readonly QUALIFIED: "versiq:qualified";
|
|
59
|
+
readonly ERROR: "versiq:error";
|
|
60
|
+
readonly QUOTA_WARNING: "versiq:quota-warning";
|
|
61
|
+
readonly QUOTA_EXCEEDED: "versiq:quota-exceeded";
|
|
62
|
+
readonly FUNNEL_STAGE_CHANGE: "versiq:funnel-stage-change";
|
|
63
|
+
readonly CTA_SHOWN: "versiq:cta-shown";
|
|
64
|
+
readonly CTA_CLICKED: "versiq:cta-clicked";
|
|
65
|
+
readonly LEAD_CAPTURED: "versiq:lead-captured";
|
|
66
|
+
readonly IDENTITY_VERIFIED: "versiq:identity-verified";
|
|
67
|
+
};
|
|
68
|
+
export type MessageType = (typeof MESSAGE_TYPES)[keyof typeof MESSAGE_TYPES];
|
|
69
|
+
export type WidgetCommand = {
|
|
70
|
+
type: typeof MESSAGE_TYPES.INIT;
|
|
71
|
+
config: WidgetConfig;
|
|
72
|
+
} | {
|
|
73
|
+
type: typeof MESSAGE_TYPES.OPEN;
|
|
74
|
+
} | {
|
|
75
|
+
type: typeof MESSAGE_TYPES.CLOSE;
|
|
76
|
+
} | {
|
|
77
|
+
type: typeof MESSAGE_TYPES.RESET;
|
|
78
|
+
} | {
|
|
79
|
+
type: typeof MESSAGE_TYPES.SET_THEME;
|
|
80
|
+
theme: ThemeConfig;
|
|
81
|
+
} | {
|
|
82
|
+
type: typeof MESSAGE_TYPES.VIEWPORT;
|
|
83
|
+
mode: ViewportMode;
|
|
84
|
+
} | {
|
|
85
|
+
type: typeof MESSAGE_TYPES.IDENTIFY;
|
|
86
|
+
email: string;
|
|
87
|
+
userId?: string;
|
|
88
|
+
userHash: string;
|
|
89
|
+
};
|
|
90
|
+
export type WidgetEvent = {
|
|
91
|
+
type: typeof MESSAGE_TYPES.READY;
|
|
92
|
+
} | {
|
|
93
|
+
type: typeof MESSAGE_TYPES.PROFILE_UPDATE;
|
|
94
|
+
} | {
|
|
95
|
+
type: typeof MESSAGE_TYPES.MESSAGE;
|
|
96
|
+
message: WidgetMessage;
|
|
97
|
+
} | {
|
|
98
|
+
type: typeof MESSAGE_TYPES.QUALIFIED;
|
|
99
|
+
} | {
|
|
100
|
+
type: typeof MESSAGE_TYPES.ERROR;
|
|
101
|
+
code: string;
|
|
102
|
+
message: string;
|
|
103
|
+
} | {
|
|
104
|
+
type: typeof MESSAGE_TYPES.OPEN;
|
|
105
|
+
} | {
|
|
106
|
+
type: typeof MESSAGE_TYPES.CLOSE;
|
|
107
|
+
} | {
|
|
108
|
+
type: typeof MESSAGE_TYPES.QUOTA_WARNING;
|
|
109
|
+
remaining: number;
|
|
110
|
+
limit: number;
|
|
111
|
+
} | {
|
|
112
|
+
type: typeof MESSAGE_TYPES.QUOTA_EXCEEDED;
|
|
113
|
+
} | {
|
|
114
|
+
type: typeof MESSAGE_TYPES.FUNNEL_STAGE_CHANGE;
|
|
115
|
+
stage: string;
|
|
116
|
+
previousStage: string | null;
|
|
117
|
+
} | {
|
|
118
|
+
type: typeof MESSAGE_TYPES.CTA_SHOWN;
|
|
119
|
+
ctaType: string;
|
|
120
|
+
objectiveId?: string;
|
|
121
|
+
} | {
|
|
122
|
+
type: typeof MESSAGE_TYPES.CTA_CLICKED;
|
|
123
|
+
ctaType: string;
|
|
124
|
+
objectiveId?: string;
|
|
125
|
+
} | {
|
|
126
|
+
type: typeof MESSAGE_TYPES.LEAD_CAPTURED;
|
|
127
|
+
hasEmail: boolean;
|
|
128
|
+
hasPhone: boolean;
|
|
129
|
+
} | {
|
|
130
|
+
type: typeof MESSAGE_TYPES.IDENTITY_VERIFIED;
|
|
131
|
+
email: string;
|
|
132
|
+
userId?: string;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Short keys (no `versiq:` prefix) used by the widget public API (on/off).
|
|
136
|
+
*/
|
|
137
|
+
export type WidgetEventType = "ready" | "profile-update" | "message" | "qualified" | "error" | "open" | "close" | "quota-warning" | "quota-exceeded" | "funnel-stage-change" | "cta-shown" | "cta-clicked" | "lead-captured" | "identity-verified" | "widget-unavailable";
|
|
138
|
+
/**
|
|
139
|
+
* Payload emitted with the `widget-unavailable` SDK event (#867).
|
|
140
|
+
*
|
|
141
|
+
* Fired by the SDK (parent side) when the iframe loader exhausted all
|
|
142
|
+
* retries — distinct from the iframe-emitted `versiq:error` which signals
|
|
143
|
+
* a runtime failure of an already-loaded widget.
|
|
144
|
+
*
|
|
145
|
+
* Dual-emission contract: when the loader fails, the SDK emits BOTH
|
|
146
|
+
* 1. `widget-unavailable` (this payload) — the canonical SDK-side event
|
|
147
|
+
* 2. `error` with `code: "IFRAME_UNAVAILABLE"` — preserved so hosts that
|
|
148
|
+
* subscribed to `error` before #867 still observe the failure
|
|
149
|
+
* without having to migrate. New integrations SHOULD prefer
|
|
150
|
+
* `widget-unavailable` for SDK-load failures and reserve `error`
|
|
151
|
+
* for runtime errors surfaced by the loaded widget.
|
|
152
|
+
*/
|
|
153
|
+
export declare const widgetUnavailablePayloadSchema: z.ZodObject<{
|
|
154
|
+
reason: z.ZodEnum<{
|
|
155
|
+
error: "error";
|
|
156
|
+
timeout: "timeout";
|
|
157
|
+
}>;
|
|
158
|
+
attempts: z.ZodNumber;
|
|
159
|
+
}, z.core.$strip>;
|
|
160
|
+
export type WidgetUnavailablePayload = z.infer<typeof widgetUnavailablePayloadSchema>;
|
|
161
|
+
export type WidgetEventHandler<T = unknown> = (data: T) => void;
|
|
162
|
+
export { themeConfigSchema, viewportModeSchema, widgetConfigSchema };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postMessage protocol between the widget iframe and its host page.
|
|
3
|
+
*
|
|
4
|
+
* Two directions share the `versiq:*` prefix:
|
|
5
|
+
* - `WidgetCommand` — parent page → iframe (commands)
|
|
6
|
+
* - `WidgetEvent` — iframe → parent page (events)
|
|
7
|
+
*
|
|
8
|
+
* The `MESSAGE_TYPES` const aggregates every message type string across both
|
|
9
|
+
* directions. Consumers that treat the protocol opaquely (e.g. origin
|
|
10
|
+
* validation helpers) can rely on it to check `type.startsWith("versiq:")`
|
|
11
|
+
* without enumerating every member.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { themeConfigSchema } from "./theme";
|
|
15
|
+
import { viewportModeSchema } from "./viewport";
|
|
16
|
+
import { widgetConfigSchema } from "./widget-config";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Message primitives
|
|
19
|
+
// ============================================================================
|
|
20
|
+
export const messageRoleSchema = z
|
|
21
|
+
.enum(["user", "assistant"])
|
|
22
|
+
.describe("Message sender role");
|
|
23
|
+
export const widgetMessageSchema = z.object({
|
|
24
|
+
role: messageRoleSchema.describe("Message sender role"),
|
|
25
|
+
content: z
|
|
26
|
+
.string()
|
|
27
|
+
.max(10000)
|
|
28
|
+
.describe("Message content (max 10000 characters)"),
|
|
29
|
+
});
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Message type catalogue
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Exhaustive catalogue of every `versiq:*` message type used in either
|
|
35
|
+
* direction. Kept as a single const so a new event or command is declared in
|
|
36
|
+
* one place.
|
|
37
|
+
*/
|
|
38
|
+
export const MESSAGE_TYPES = {
|
|
39
|
+
// Parent → iframe (commands)
|
|
40
|
+
INIT: "versiq:init",
|
|
41
|
+
OPEN: "versiq:open",
|
|
42
|
+
CLOSE: "versiq:close",
|
|
43
|
+
RESET: "versiq:reset",
|
|
44
|
+
SET_THEME: "versiq:setTheme",
|
|
45
|
+
VIEWPORT: "versiq:viewport",
|
|
46
|
+
IDENTIFY: "versiq:identify",
|
|
47
|
+
// iframe → parent (events)
|
|
48
|
+
READY: "versiq:ready",
|
|
49
|
+
PROFILE_UPDATE: "versiq:profile-update",
|
|
50
|
+
MESSAGE: "versiq:message",
|
|
51
|
+
QUALIFIED: "versiq:qualified",
|
|
52
|
+
ERROR: "versiq:error",
|
|
53
|
+
QUOTA_WARNING: "versiq:quota-warning",
|
|
54
|
+
QUOTA_EXCEEDED: "versiq:quota-exceeded",
|
|
55
|
+
FUNNEL_STAGE_CHANGE: "versiq:funnel-stage-change",
|
|
56
|
+
CTA_SHOWN: "versiq:cta-shown",
|
|
57
|
+
CTA_CLICKED: "versiq:cta-clicked",
|
|
58
|
+
LEAD_CAPTURED: "versiq:lead-captured",
|
|
59
|
+
IDENTITY_VERIFIED: "versiq:identity-verified",
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Payload emitted with the `widget-unavailable` SDK event (#867).
|
|
63
|
+
*
|
|
64
|
+
* Fired by the SDK (parent side) when the iframe loader exhausted all
|
|
65
|
+
* retries — distinct from the iframe-emitted `versiq:error` which signals
|
|
66
|
+
* a runtime failure of an already-loaded widget.
|
|
67
|
+
*
|
|
68
|
+
* Dual-emission contract: when the loader fails, the SDK emits BOTH
|
|
69
|
+
* 1. `widget-unavailable` (this payload) — the canonical SDK-side event
|
|
70
|
+
* 2. `error` with `code: "IFRAME_UNAVAILABLE"` — preserved so hosts that
|
|
71
|
+
* subscribed to `error` before #867 still observe the failure
|
|
72
|
+
* without having to migrate. New integrations SHOULD prefer
|
|
73
|
+
* `widget-unavailable` for SDK-load failures and reserve `error`
|
|
74
|
+
* for runtime errors surfaced by the loaded widget.
|
|
75
|
+
*/
|
|
76
|
+
export const widgetUnavailablePayloadSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
reason: z
|
|
79
|
+
.enum(["timeout", "error"])
|
|
80
|
+
.describe("Why the iframe load failed (timeout vs. transport error)"),
|
|
81
|
+
attempts: z
|
|
82
|
+
.number()
|
|
83
|
+
.int()
|
|
84
|
+
.min(1)
|
|
85
|
+
.describe("Total attempts made before giving up (e.g. 3 = 1 + 2 retries)"),
|
|
86
|
+
})
|
|
87
|
+
.describe("Payload emitted with the widget-unavailable SDK event (#867)");
|
|
88
|
+
// Re-export schemas that participate in the protocol for consumers that want
|
|
89
|
+
// runtime validation (e.g. widget bootstrap parsing a `WidgetConfig`).
|
|
90
|
+
export { themeConfigSchema, viewportModeSchema, widgetConfigSchema };
|
|
91
|
+
// `widgetUnavailablePayloadSchema` is declared above with its derived type.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session cookie naming + session-id validation — the single source of truth
|
|
3
|
+
* shared by the app server (`apps/app/src/lib/security/session-cookie.ts`) and
|
|
4
|
+
* the E2E browser suite (`tests/e2e-browser`). Extracted here (#918 review) to
|
|
5
|
+
* remove the two previously-duplicated copies that could silently desync: a
|
|
6
|
+
* drift in the cookie prefix or the sanitization regex would otherwise break
|
|
7
|
+
* sessionId capture only in the browser run, a failure hard to attribute.
|
|
8
|
+
*
|
|
9
|
+
* **This module MUST stay import-free (pure)**, like `./motion` — it is exposed
|
|
10
|
+
* via the dedicated subpath `@gamaro.io/core-types/session` so a consumer
|
|
11
|
+
* (e.g. the test workspace, the vanilla loader) can pull just these helpers
|
|
12
|
+
* without dragging Zod in via the barrel `index.ts`.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Per-vertical session cookie name. Each vertical owns its own cookie to prevent
|
|
16
|
+
* profile-schema conflicts. Any character outside `[a-z0-9-]` (case-insensitive)
|
|
17
|
+
* is sanitized to `_` (each char maps to one `_`, no collapsing), so
|
|
18
|
+
* `real-estate` stays `versiq_session_real-estate`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getSessionCookieName(verticalId: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Validate the UUID shape of a session id. The length pre-check is
|
|
23
|
+
* defense-in-depth (avoids running the regex on arbitrarily long strings).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isValidSessionId(value: string): boolean;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session cookie naming + session-id validation — the single source of truth
|
|
3
|
+
* shared by the app server (`apps/app/src/lib/security/session-cookie.ts`) and
|
|
4
|
+
* the E2E browser suite (`tests/e2e-browser`). Extracted here (#918 review) to
|
|
5
|
+
* remove the two previously-duplicated copies that could silently desync: a
|
|
6
|
+
* drift in the cookie prefix or the sanitization regex would otherwise break
|
|
7
|
+
* sessionId capture only in the browser run, a failure hard to attribute.
|
|
8
|
+
*
|
|
9
|
+
* **This module MUST stay import-free (pure)**, like `./motion` — it is exposed
|
|
10
|
+
* via the dedicated subpath `@gamaro.io/core-types/session` so a consumer
|
|
11
|
+
* (e.g. the test workspace, the vanilla loader) can pull just these helpers
|
|
12
|
+
* without dragging Zod in via the barrel `index.ts`.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Per-vertical session cookie name. Each vertical owns its own cookie to prevent
|
|
16
|
+
* profile-schema conflicts. Any character outside `[a-z0-9-]` (case-insensitive)
|
|
17
|
+
* is sanitized to `_` (each char maps to one `_`, no collapsing), so
|
|
18
|
+
* `real-estate` stays `versiq_session_real-estate`.
|
|
19
|
+
*/
|
|
20
|
+
export function getSessionCookieName(verticalId) {
|
|
21
|
+
const sanitized = verticalId.replace(/[^a-z0-9-]/gi, "_");
|
|
22
|
+
return `versiq_session_${sanitized}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validate the UUID shape of a session id. The length pre-check is
|
|
26
|
+
* defense-in-depth (avoids running the regex on arbitrarily long strings).
|
|
27
|
+
*/
|
|
28
|
+
export function isValidSessionId(value) {
|
|
29
|
+
if (value.length !== 36)
|
|
30
|
+
return false;
|
|
31
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme resolver — single source of truth for `ThemeConfig` → CSS-ready values.
|
|
3
|
+
*
|
|
4
|
+
* Pure function consumed by:
|
|
5
|
+
* - `apps/app/src/app/widget/embed/EmbedChat.tsx` (iframe runtime)
|
|
6
|
+
* - `apps/app/src/components/portal/sandbox/WidgetMockPreview.tsx` (admin preview)
|
|
7
|
+
*
|
|
8
|
+
* Before this module existed, the same resolution rules were duplicated across
|
|
9
|
+
* the two call sites (cf. audit `tmp/widget-customization-review.md` § 3.4 —
|
|
10
|
+
* fuite n° 3 of the verdict "abstraction qui ne tient pas sa promesse"). Any
|
|
11
|
+
* evolution had to be mirrored by hand, with no compiler check. The function
|
|
12
|
+
* here is the contract; both callers MUST consume it.
|
|
13
|
+
*
|
|
14
|
+
* Dark-mode policy:
|
|
15
|
+
* `backgroundColor` and `textColor` admin-side hex overrides apply in
|
|
16
|
+
* LIGHT MODE ONLY. The widget's `colorScheme: "dark"` token flips surface
|
|
17
|
+
* tokens via the `.dark` CSS class on the iframe root — forcing a saved
|
|
18
|
+
* "#FFFFFF" through dark mode would defeat the toggle and yield a
|
|
19
|
+
* white-on-white outcome on the visitor's site. When `isDark` is true,
|
|
20
|
+
* those two fields fall back to the resolver's `defaults` (typically CSS
|
|
21
|
+
* variables that respond to `.dark`).
|
|
22
|
+
*
|
|
23
|
+
* `primaryColor` is brand-side and applies in both schemes: the assistant
|
|
24
|
+
* bubble and the launcher button must stay the brand colour even on a
|
|
25
|
+
* dark surface.
|
|
26
|
+
*/
|
|
27
|
+
import type { ThemeConfig } from "./theme";
|
|
28
|
+
/**
|
|
29
|
+
* Caller-supplied fallback palette. Each consumer (runtime, preview) passes
|
|
30
|
+
* the same constant — that is what guarantees alignment by construction. The
|
|
31
|
+
* test contract (`theme-resolver.test.ts`) asserts identical output for
|
|
32
|
+
* identical input across callers.
|
|
33
|
+
*/
|
|
34
|
+
export interface ThemeDefaults {
|
|
35
|
+
/** Brand colour applied to launcher + user bubble. Used in BOTH schemes. */
|
|
36
|
+
primaryColor: string;
|
|
37
|
+
/** Background applied to the widget surface. Used only when `isDark === false`. */
|
|
38
|
+
backgroundColor: string;
|
|
39
|
+
/** Foreground text colour. Used only when `isDark === false`. */
|
|
40
|
+
textColor: string;
|
|
41
|
+
/** Default radius (in pixels) used when `theme.borderRadius == null`. */
|
|
42
|
+
borderRadiusPx: number;
|
|
43
|
+
/** Default font stack used when `theme.fontFamily == null`. */
|
|
44
|
+
fontFamily: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ResolvedTheme {
|
|
47
|
+
primaryColor: string;
|
|
48
|
+
backgroundColor: string;
|
|
49
|
+
textColor: string;
|
|
50
|
+
/** CSS string ready to apply (e.g. `"16px"`). */
|
|
51
|
+
borderRadius: string;
|
|
52
|
+
fontFamily: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function resolveTheme(theme: ThemeConfig | null | undefined, isDark: boolean, defaults: ThemeDefaults): ResolvedTheme;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme resolver — single source of truth for `ThemeConfig` → CSS-ready values.
|
|
3
|
+
*
|
|
4
|
+
* Pure function consumed by:
|
|
5
|
+
* - `apps/app/src/app/widget/embed/EmbedChat.tsx` (iframe runtime)
|
|
6
|
+
* - `apps/app/src/components/portal/sandbox/WidgetMockPreview.tsx` (admin preview)
|
|
7
|
+
*
|
|
8
|
+
* Before this module existed, the same resolution rules were duplicated across
|
|
9
|
+
* the two call sites (cf. audit `tmp/widget-customization-review.md` § 3.4 —
|
|
10
|
+
* fuite n° 3 of the verdict "abstraction qui ne tient pas sa promesse"). Any
|
|
11
|
+
* evolution had to be mirrored by hand, with no compiler check. The function
|
|
12
|
+
* here is the contract; both callers MUST consume it.
|
|
13
|
+
*
|
|
14
|
+
* Dark-mode policy:
|
|
15
|
+
* `backgroundColor` and `textColor` admin-side hex overrides apply in
|
|
16
|
+
* LIGHT MODE ONLY. The widget's `colorScheme: "dark"` token flips surface
|
|
17
|
+
* tokens via the `.dark` CSS class on the iframe root — forcing a saved
|
|
18
|
+
* "#FFFFFF" through dark mode would defeat the toggle and yield a
|
|
19
|
+
* white-on-white outcome on the visitor's site. When `isDark` is true,
|
|
20
|
+
* those two fields fall back to the resolver's `defaults` (typically CSS
|
|
21
|
+
* variables that respond to `.dark`).
|
|
22
|
+
*
|
|
23
|
+
* `primaryColor` is brand-side and applies in both schemes: the assistant
|
|
24
|
+
* bubble and the launcher button must stay the brand colour even on a
|
|
25
|
+
* dark surface.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveTheme(theme, isDark, defaults) {
|
|
28
|
+
const primaryColor = theme?.primaryColor ?? defaults.primaryColor;
|
|
29
|
+
const backgroundColor = isDark
|
|
30
|
+
? defaults.backgroundColor
|
|
31
|
+
: (theme?.backgroundColor ?? defaults.backgroundColor);
|
|
32
|
+
const textColor = isDark
|
|
33
|
+
? defaults.textColor
|
|
34
|
+
: (theme?.textColor ?? defaults.textColor);
|
|
35
|
+
const borderRadius = theme?.borderRadius != null
|
|
36
|
+
? `${theme.borderRadius}px`
|
|
37
|
+
: `${defaults.borderRadiusPx}px`;
|
|
38
|
+
const fontFamily = theme?.fontFamily ?? defaults.fontFamily;
|
|
39
|
+
return { primaryColor, backgroundColor, textColor, borderRadius, fontFamily };
|
|
40
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme configuration schema for customizing widget appearance.
|
|
3
|
+
*
|
|
4
|
+
* Cross-boundary contract shared by the widget SDK (`@gamaro.io/widget`) and the
|
|
5
|
+
* app host (`apps/app`). Both workspaces historically redefined the same
|
|
6
|
+
* fields — this module is the single source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
export declare const themeConfigSchema: z.ZodObject<{
|
|
10
|
+
primaryColor: z.ZodOptional<z.ZodString>;
|
|
11
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
12
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
13
|
+
borderRadius: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
fontFamily: z.ZodOptional<z.ZodString>;
|
|
15
|
+
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
light: "light";
|
|
17
|
+
dark: "dark";
|
|
18
|
+
auto: "auto";
|
|
19
|
+
}>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type ThemeConfig = z.infer<typeof themeConfigSchema>;
|
|
22
|
+
export type ColorScheme = NonNullable<ThemeConfig["colorScheme"]>;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme configuration schema for customizing widget appearance.
|
|
3
|
+
*
|
|
4
|
+
* Cross-boundary contract shared by the widget SDK (`@gamaro.io/widget`) and the
|
|
5
|
+
* app host (`apps/app`). Both workspaces historically redefined the same
|
|
6
|
+
* fields — this module is the single source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
// 6-digit hex color (e.g. "#3B82F6"). Lower or upper case accepted.
|
|
10
|
+
// `max(20)` is kept on top of the regex as a belt-and-braces guard against
|
|
11
|
+
// pre-validation length attacks even if the regex were ever loosened.
|
|
12
|
+
const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
|
13
|
+
const HEX_COLOR_MESSAGE = "Color must be a 6-digit hex code (e.g. '#3B82F6'). Short hex, rgb(), and named colors are not supported.";
|
|
14
|
+
export const themeConfigSchema = z.object({
|
|
15
|
+
primaryColor: z
|
|
16
|
+
.string()
|
|
17
|
+
.max(20)
|
|
18
|
+
.regex(HEX_COLOR_REGEX, HEX_COLOR_MESSAGE)
|
|
19
|
+
.describe("Primary brand color (hex format, e.g., '#3B82F6')")
|
|
20
|
+
.optional(),
|
|
21
|
+
backgroundColor: z
|
|
22
|
+
.string()
|
|
23
|
+
.max(20)
|
|
24
|
+
.regex(HEX_COLOR_REGEX, HEX_COLOR_MESSAGE)
|
|
25
|
+
.describe("Background color for the widget container")
|
|
26
|
+
.optional(),
|
|
27
|
+
textColor: z
|
|
28
|
+
.string()
|
|
29
|
+
.max(20)
|
|
30
|
+
.regex(HEX_COLOR_REGEX, HEX_COLOR_MESSAGE)
|
|
31
|
+
.describe("Text color")
|
|
32
|
+
.optional(),
|
|
33
|
+
borderRadius: z.number().describe("Border radius in pixels").optional(),
|
|
34
|
+
fontFamily: z.string().max(100).describe("Font family").optional(),
|
|
35
|
+
colorScheme: z
|
|
36
|
+
.enum(["light", "dark", "auto"])
|
|
37
|
+
.describe("Widget color scheme: 'light' (default), 'dark', or 'auto' (follows the visitor's prefers-color-scheme).")
|
|
38
|
+
.optional(),
|
|
39
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport, breakpoints, responsive and position contracts for the widget.
|
|
3
|
+
*
|
|
4
|
+
* Cross-boundary contract shared by `@gamaro.io/widget` (runtime enforcement) and
|
|
5
|
+
* `apps/app` (sandbox preview, embed layout).
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
export declare const viewportModeSchema: z.ZodEnum<{
|
|
9
|
+
mobile: "mobile";
|
|
10
|
+
tablet: "tablet";
|
|
11
|
+
desktop: "desktop";
|
|
12
|
+
}>;
|
|
13
|
+
export type ViewportMode = z.infer<typeof viewportModeSchema>;
|
|
14
|
+
export declare const widgetPositionSchema: z.ZodEnum<{
|
|
15
|
+
"bottom-right": "bottom-right";
|
|
16
|
+
"bottom-left": "bottom-left";
|
|
17
|
+
inline: "inline";
|
|
18
|
+
}>;
|
|
19
|
+
export type WidgetPosition = z.infer<typeof widgetPositionSchema>;
|
|
20
|
+
export declare const breakpointsConfigSchema: z.ZodObject<{
|
|
21
|
+
mobile: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
tablet: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type BreakpointsConfig = z.infer<typeof breakpointsConfigSchema>;
|
|
25
|
+
export declare const responsiveConfigSchema: z.ZodObject<{
|
|
26
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
breakpoints: z.ZodOptional<z.ZodObject<{
|
|
28
|
+
mobile: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
tablet: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
export type ResponsiveConfig = z.infer<typeof responsiveConfigSchema>;
|
package/dist/viewport.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport, breakpoints, responsive and position contracts for the widget.
|
|
3
|
+
*
|
|
4
|
+
* Cross-boundary contract shared by `@gamaro.io/widget` (runtime enforcement) and
|
|
5
|
+
* `apps/app` (sandbox preview, embed layout).
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
export const viewportModeSchema = z
|
|
9
|
+
.enum(["mobile", "tablet", "desktop"])
|
|
10
|
+
.describe("Viewport mode: mobile (<480px), tablet (480-768px), desktop (>768px)");
|
|
11
|
+
export const widgetPositionSchema = z
|
|
12
|
+
.enum(["bottom-right", "bottom-left", "inline"])
|
|
13
|
+
.describe("Widget position on the page");
|
|
14
|
+
export const breakpointsConfigSchema = z.object({
|
|
15
|
+
mobile: z
|
|
16
|
+
.number()
|
|
17
|
+
.min(0)
|
|
18
|
+
.max(1000)
|
|
19
|
+
.describe("Max width for mobile viewport (default: 480)")
|
|
20
|
+
.optional(),
|
|
21
|
+
tablet: z
|
|
22
|
+
.number()
|
|
23
|
+
.min(0)
|
|
24
|
+
.max(2000)
|
|
25
|
+
.describe("Max width for tablet viewport (default: 768)")
|
|
26
|
+
.optional(),
|
|
27
|
+
});
|
|
28
|
+
export const responsiveConfigSchema = z.object({
|
|
29
|
+
enabled: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.describe("Enable responsive behavior (default: true)")
|
|
32
|
+
.optional(),
|
|
33
|
+
breakpoints: breakpointsConfigSchema
|
|
34
|
+
.describe("Custom breakpoint values")
|
|
35
|
+
.optional(),
|
|
36
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full widget configuration contract.
|
|
3
|
+
*
|
|
4
|
+
* Consumed by `@gamaro.io/widget` (config parsing and iframe bootstrap) and by
|
|
5
|
+
* `apps/app` sandbox/preview components via `@gamaro.io/widget`'s re-export.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
export declare const widgetConfigSchema: z.ZodObject<{
|
|
9
|
+
theme: z.ZodOptional<z.ZodObject<{
|
|
10
|
+
primaryColor: z.ZodOptional<z.ZodString>;
|
|
11
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
12
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
13
|
+
borderRadius: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
fontFamily: z.ZodOptional<z.ZodString>;
|
|
15
|
+
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
light: "light";
|
|
17
|
+
dark: "dark";
|
|
18
|
+
auto: "auto";
|
|
19
|
+
}>>;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
position: z.ZodOptional<z.ZodEnum<{
|
|
22
|
+
"bottom-right": "bottom-right";
|
|
23
|
+
"bottom-left": "bottom-left";
|
|
24
|
+
inline: "inline";
|
|
25
|
+
}>>;
|
|
26
|
+
open: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
container: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<HTMLElement, HTMLElement>, z.ZodString]>>;
|
|
28
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
29
|
+
debug: z.ZodOptional<z.ZodBoolean>;
|
|
30
|
+
responsive: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
breakpoints: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
mobile: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
tablet: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
showProfile: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
language: z.ZodOptional<z.ZodString>;
|
|
39
|
+
apiKey: z.ZodString;
|
|
40
|
+
email: z.ZodOptional<z.ZodString>;
|
|
41
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
42
|
+
userHash: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
export type WidgetConfig = z.infer<typeof widgetConfigSchema>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full widget configuration contract.
|
|
3
|
+
*
|
|
4
|
+
* Consumed by `@gamaro.io/widget` (config parsing and iframe bootstrap) and by
|
|
5
|
+
* `apps/app` sandbox/preview components via `@gamaro.io/widget`'s re-export.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { themeConfigSchema } from "./theme";
|
|
9
|
+
import { responsiveConfigSchema, widgetPositionSchema } from "./viewport";
|
|
10
|
+
export const widgetConfigSchema = z.object({
|
|
11
|
+
theme: themeConfigSchema.describe("Custom theme configuration").optional(),
|
|
12
|
+
position: widgetPositionSchema
|
|
13
|
+
.describe("Widget position (default: 'bottom-right')")
|
|
14
|
+
.optional(),
|
|
15
|
+
open: z.boolean().describe("Initial open state (default: false)").optional(),
|
|
16
|
+
container: z
|
|
17
|
+
.union([
|
|
18
|
+
z.custom((val) => val instanceof HTMLElement),
|
|
19
|
+
z.string().max(200),
|
|
20
|
+
])
|
|
21
|
+
.describe("Container element for inline mode")
|
|
22
|
+
.optional(),
|
|
23
|
+
baseUrl: z
|
|
24
|
+
.string()
|
|
25
|
+
.url()
|
|
26
|
+
.max(200)
|
|
27
|
+
.describe("Base URL for the widget embed (default: production URL)")
|
|
28
|
+
.optional(),
|
|
29
|
+
debug: z.boolean().describe("Enable debug logging").optional(),
|
|
30
|
+
responsive: responsiveConfigSchema
|
|
31
|
+
.describe("Responsive behavior configuration")
|
|
32
|
+
.optional(),
|
|
33
|
+
showProfile: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.describe("Show profile panel in widget header (default: false)")
|
|
36
|
+
.optional(),
|
|
37
|
+
language: z
|
|
38
|
+
.string()
|
|
39
|
+
.max(10)
|
|
40
|
+
.describe("Language for UI labels (e.g., 'fr', 'en'). Defaults to browser language.")
|
|
41
|
+
.optional(),
|
|
42
|
+
apiKey: z
|
|
43
|
+
.string()
|
|
44
|
+
.max(100)
|
|
45
|
+
.describe("Publishable API key (pk_*) for application authentication"),
|
|
46
|
+
email: z
|
|
47
|
+
.string()
|
|
48
|
+
.email()
|
|
49
|
+
.max(255)
|
|
50
|
+
.describe("Pre-identified user email (host-attested identity)")
|
|
51
|
+
.optional(),
|
|
52
|
+
userId: z
|
|
53
|
+
.string()
|
|
54
|
+
.max(255)
|
|
55
|
+
.describe("Host-side unique user identifier")
|
|
56
|
+
.optional(),
|
|
57
|
+
userHash: z
|
|
58
|
+
.string()
|
|
59
|
+
.max(128)
|
|
60
|
+
.describe("HMAC-SHA256 of email (or userId), signed with identity secret")
|
|
61
|
+
.optional(),
|
|
62
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget runtime config — server-authoritative, admin-editable.
|
|
3
|
+
*
|
|
4
|
+
* This schema defines what the admin portal can edit for a given Application
|
|
5
|
+
* and what the widget SDK receives at bootstrap via `GET /api/application/config`.
|
|
6
|
+
* It is deliberately disjoint from `widgetConfigSchema` (which contains the
|
|
7
|
+
* host-context attributes `apiKey`/`container`/`baseUrl`/`debug`/identity
|
|
8
|
+
* fields that can only live on the integrator's page).
|
|
9
|
+
*
|
|
10
|
+
* Single source of truth consumed by:
|
|
11
|
+
* - `apps/app` admin route handlers (Zod second-gate on response + PUT body)
|
|
12
|
+
* - `apps/app` portal widget editor (form schema)
|
|
13
|
+
* - `packages/widget` config resolver (server-wins merge policy)
|
|
14
|
+
*
|
|
15
|
+
* Storage: `applications.widget_config` JSONB column. Cache-invalidated on
|
|
16
|
+
* update via `invalidateKeyCacheForApplication`.
|
|
17
|
+
*
|
|
18
|
+
* Part of #726 — unifying widget config so the integration reduces to
|
|
19
|
+
* `<script data-api-key="pk_live_…"></script>`.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
export declare const widgetRuntimeConfigSchema: z.ZodObject<{
|
|
23
|
+
theme: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
primaryColor: z.ZodOptional<z.ZodString>;
|
|
25
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
26
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
27
|
+
borderRadius: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
fontFamily: z.ZodOptional<z.ZodString>;
|
|
29
|
+
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
light: "light";
|
|
31
|
+
dark: "dark";
|
|
32
|
+
auto: "auto";
|
|
33
|
+
}>>;
|
|
34
|
+
}, z.core.$strip>>;
|
|
35
|
+
position: z.ZodOptional<z.ZodEnum<{
|
|
36
|
+
"bottom-right": "bottom-right";
|
|
37
|
+
"bottom-left": "bottom-left";
|
|
38
|
+
inline: "inline";
|
|
39
|
+
}>>;
|
|
40
|
+
language: z.ZodOptional<z.ZodString>;
|
|
41
|
+
showProfile: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
open: z.ZodOptional<z.ZodBoolean>;
|
|
43
|
+
brand: z.ZodOptional<z.ZodObject<{
|
|
44
|
+
title: z.ZodOptional<z.ZodString>;
|
|
45
|
+
avatarUrl: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
export type WidgetRuntimeConfig = z.infer<typeof widgetRuntimeConfigSchema>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget runtime config — server-authoritative, admin-editable.
|
|
3
|
+
*
|
|
4
|
+
* This schema defines what the admin portal can edit for a given Application
|
|
5
|
+
* and what the widget SDK receives at bootstrap via `GET /api/application/config`.
|
|
6
|
+
* It is deliberately disjoint from `widgetConfigSchema` (which contains the
|
|
7
|
+
* host-context attributes `apiKey`/`container`/`baseUrl`/`debug`/identity
|
|
8
|
+
* fields that can only live on the integrator's page).
|
|
9
|
+
*
|
|
10
|
+
* Single source of truth consumed by:
|
|
11
|
+
* - `apps/app` admin route handlers (Zod second-gate on response + PUT body)
|
|
12
|
+
* - `apps/app` portal widget editor (form schema)
|
|
13
|
+
* - `packages/widget` config resolver (server-wins merge policy)
|
|
14
|
+
*
|
|
15
|
+
* Storage: `applications.widget_config` JSONB column. Cache-invalidated on
|
|
16
|
+
* update via `invalidateKeyCacheForApplication`.
|
|
17
|
+
*
|
|
18
|
+
* Part of #726 — unifying widget config so the integration reduces to
|
|
19
|
+
* `<script data-api-key="pk_live_…"></script>`.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { themeConfigSchema } from "./theme";
|
|
23
|
+
import { widgetPositionSchema } from "./viewport";
|
|
24
|
+
export const widgetRuntimeConfigSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
theme: themeConfigSchema
|
|
27
|
+
.describe("Full visual theme palette applied by the widget. Admin-configured, no client-side override.")
|
|
28
|
+
.optional(),
|
|
29
|
+
position: widgetPositionSchema
|
|
30
|
+
.describe("Default launcher position. `inline` only applies when integrator supplies `data-container`.")
|
|
31
|
+
.optional(),
|
|
32
|
+
language: z
|
|
33
|
+
.string()
|
|
34
|
+
.max(10)
|
|
35
|
+
.describe("Locale forced for widget UI labels (ISO 639-1). Falls back to browser language when null.")
|
|
36
|
+
.optional(),
|
|
37
|
+
showProfile: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.describe("Whether the widget header exposes the profile panel to end-users.")
|
|
40
|
+
.optional(),
|
|
41
|
+
open: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.describe("Default open state on load. Host can still call `window.Versiq.open()` programmatically.")
|
|
44
|
+
.optional(),
|
|
45
|
+
brand: z
|
|
46
|
+
.object({
|
|
47
|
+
title: z
|
|
48
|
+
.string()
|
|
49
|
+
.max(60)
|
|
50
|
+
.describe("Custom widget header title. Falls back to a vertical-specific default (e.g. 'Versiq Chat' for B2B) when unset.")
|
|
51
|
+
.optional(),
|
|
52
|
+
avatarUrl: z
|
|
53
|
+
.string()
|
|
54
|
+
.url()
|
|
55
|
+
.max(2048)
|
|
56
|
+
.describe("Public URL of the custom widget avatar (PNG or JPEG). MUST originate from the branding-upload endpoint — arbitrary external URLs are rejected at the API boundary.")
|
|
57
|
+
.optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
.describe("White-label branding overrides (issue #1083 Piste 5). Both fields are optional.")
|
|
61
|
+
.optional(),
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gamaro.io/core-types",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Cross-workspace TypeScript contracts and Zod schemas consumed by @gamaro.io/widget. Intended as a peer of the widget SDK.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./motion": {
|
|
16
|
+
"types": "./dist/motion.d.ts",
|
|
17
|
+
"import": "./dist/motion.js",
|
|
18
|
+
"default": "./dist/motion.js"
|
|
19
|
+
},
|
|
20
|
+
"./session": {
|
|
21
|
+
"types": "./dist/session.d.ts",
|
|
22
|
+
"import": "./dist/session.js",
|
|
23
|
+
"default": "./dist/session.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
36
|
+
"author": "Sébastien Dolard <admin@gamaro.io>",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/sdolard/Toize.git",
|
|
40
|
+
"directory": "packages/core-types"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/sdolard/Toize/tree/main/packages/core-types#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/sdolard/Toize/issues"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"gamaro",
|
|
48
|
+
"widget",
|
|
49
|
+
"types",
|
|
50
|
+
"zod",
|
|
51
|
+
"schemas",
|
|
52
|
+
"sdk"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"zod": "4.3.6"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@microsoft/api-extractor": "^7.58.12",
|
|
62
|
+
"typescript": "^6.0.3"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"clean": "rm -rf dist",
|
|
66
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
67
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"typecheck:ci": "pnpm typecheck",
|
|
70
|
+
"api-extractor:check": "api-extractor run --verbose -c api-extractor.json && api-extractor run --verbose -c api-extractor.motion.json && api-extractor run --verbose -c api-extractor.session.json",
|
|
71
|
+
"api-extractor:local": "api-extractor run --local --verbose -c api-extractor.json && api-extractor run --local --verbose -c api-extractor.motion.json && api-extractor run --local --verbose -c api-extractor.session.json"
|
|
72
|
+
}
|
|
73
|
+
}
|