@bloomscorp/blooms-ai-embed 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/CHANGELOG.md +20 -0
- package/README.md +456 -0
- package/dist/blooms-embed.css +1 -0
- package/dist/blooms-embed.js +10 -0
- package/dist/browser-guard.js +23 -0
- package/dist/index.d.ts +284 -0
- package/dist/index.js +37 -0
- package/package.json +54 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@bloomscorp/blooms-ai-embed`.
|
|
4
|
+
|
|
5
|
+
## 0.1.0
|
|
6
|
+
|
|
7
|
+
First release.
|
|
8
|
+
|
|
9
|
+
- Ships the prebuilt blooms.ai element bundle (`blooms-embed.js`) and its shared stylesheet
|
|
10
|
+
(`blooms-embed.css`) as an npm package, so hosts — Electron in particular — can bundle the
|
|
11
|
+
components locally instead of loading remote code into a renderer, and work offline.
|
|
12
|
+
- Importing the package defines the custom elements (`<blooms-login>`) as a side effect and
|
|
13
|
+
exports the typed `BloomsEmbed` SDK: `configure`, `mode`, `manifest`, `login`, `session`,
|
|
14
|
+
`resume`, `can`, `logout`, `on`.
|
|
15
|
+
- Hand-written TypeScript declarations for the whole public surface, including
|
|
16
|
+
`<blooms-login>` in `HTMLElementTagNameMap` and `BloomsEmbed` on `Window`.
|
|
17
|
+
- Importing outside a browser throws an explanatory error naming the likely cause (SSR, or the
|
|
18
|
+
Electron main process instead of the renderer) instead of failing on a missing `HTMLElement`.
|
|
19
|
+
- Integrator documentation: quick start, API reference, events and error codes, theming,
|
|
20
|
+
framework notes (plain HTML, React 19, React <= 18 wrapper, Angular), and Electron setup.
|
package/README.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# `@bloomscorp/blooms-ai-embed`
|
|
2
|
+
|
|
3
|
+
The blooms.ai embeddable UI, as a prebuilt npm package: import it and you get blooms.ai
|
|
4
|
+
components as custom elements in your own dashboard, plus a small JS SDK for sign-in and
|
|
5
|
+
session state.
|
|
6
|
+
|
|
7
|
+
It is the same bundle blooms.ai serves from its own domain, published to npm so you can ship
|
|
8
|
+
it inside your app. That matters for **Electron** — loading remote code into a renderer is
|
|
9
|
+
against Electron's security guidance — and for anything that has to work offline or behind a
|
|
10
|
+
locked-down CSP.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install @bloomscorp/blooms-ai-embed
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires a browser (or an Electron renderer). Node >= 18 to install. No runtime dependencies.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## The two credentials
|
|
21
|
+
|
|
22
|
+
A **project embed token** (`blm_embed_…`) identifies *your integration*. It is publishable —
|
|
23
|
+
it ships in your frontend — and on its own it grants nothing: no data, no project access, not
|
|
24
|
+
even a directory of users. It is only accepted from the web origins a blooms.ai admin has
|
|
25
|
+
registered on it.
|
|
26
|
+
|
|
27
|
+
A **user session** is what actually authorizes data. Each of your users signs in with their
|
|
28
|
+
own blooms.ai account, and the server returns only what that account is already allowed to see
|
|
29
|
+
in that project, narrowed further to the features enabled on your token. No session, no data.
|
|
30
|
+
|
|
31
|
+
> Ask your blooms.ai contact to register every origin you will load from (e.g.
|
|
32
|
+
> `https://app.example.com`, `http://localhost:5173`, `app://your-desktop-app`). An
|
|
33
|
+
> unregistered origin fails CORS, which browsers report to JavaScript as a generic network
|
|
34
|
+
> error — the SDK rewrites that into an explicit message, but the fix is always "register the
|
|
35
|
+
> origin".
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<script type="module">
|
|
43
|
+
import { BloomsEmbed } from '@bloomscorp/blooms-ai-embed';
|
|
44
|
+
|
|
45
|
+
BloomsEmbed.configure({
|
|
46
|
+
token: 'blm_embed_9f2c…',
|
|
47
|
+
apiBase: 'https://api.blooms.ai',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
BloomsEmbed.on('session', event => {
|
|
51
|
+
console.log(event.session ? `signed in as ${event.session.user.email}` : 'signed out');
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<blooms-login></blooms-login>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
That is the whole integration. Importing the package defines the elements and installs the SDK;
|
|
59
|
+
`<blooms-login>` renders the sign-in form, remembers the session across reloads, and revalidates
|
|
60
|
+
it on the next load.
|
|
61
|
+
|
|
62
|
+
Three things worth knowing:
|
|
63
|
+
|
|
64
|
+
- **`configure()` once, early.** Elements placed in your HTML upgrade the moment the package
|
|
65
|
+
evaluates, which is before your `configure()` call runs. They wait for it rather than failing,
|
|
66
|
+
showing a skeleton meanwhile — so a slow `configure()` is a slow first paint, not an error.
|
|
67
|
+
- **`apiBase` is optional** when you load the bundle from blooms.ai, because it defaults to the
|
|
68
|
+
bundle's own origin. When you install from npm the bundle's origin is *your* app, so **pass
|
|
69
|
+
`apiBase` explicitly**.
|
|
70
|
+
- **Place elements unconditionally.** Feature elements render a sign-in prompt until a session
|
|
71
|
+
exists; you do not have to gate them yourself.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## `BloomsEmbed` API
|
|
76
|
+
|
|
77
|
+
| Member | Signature | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `version` | `string` | Bundle version. |
|
|
80
|
+
| `configure` | `(config: BloomsEmbedConfig) => void` | Call first. Throws on a missing token or a token that is not `blm_embed_…`. |
|
|
81
|
+
| `mode` | `() => 'element' \| 'iframe' \| null` | The resolved rendering mode; `null` before `configure()`. |
|
|
82
|
+
| `manifest` | `() => Promise<EmbedManifest>` | Pre-login bootstrap: project name, enabled features, theme, branding flag, session TTL. |
|
|
83
|
+
| `login` | `({ email, password }) => Promise<EmbedSessionState>` | Programmatic alternative to `<blooms-login>`, for hosts with their own form. |
|
|
84
|
+
| `session` | `() => EmbedSessionState \| null` | The current session, restored from storage by `configure()`. Synchronous. |
|
|
85
|
+
| `resume` | `() => Promise<EmbedSessionState \| null>` | Revalidates a restored session server-side and re-reads permissions. `null` if it is no longer good. |
|
|
86
|
+
| `can` | `(featureKey: string) => boolean` | Local check against the session's `grantedKeys`. Use it for UI, never as your only gate — the server re-checks every request. |
|
|
87
|
+
| `logout` | `() => Promise<void>` | Revokes the session server-side and clears storage. |
|
|
88
|
+
| `on` | `(type, handler) => () => void` | Subscribe; returns an unsubscribe function. `type` is `'session'`, `'error'`, `'navigate'` or `'all'`. |
|
|
89
|
+
|
|
90
|
+
### `configure()` options
|
|
91
|
+
|
|
92
|
+
| Option | Type | Default | Notes |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| `token` | `string` | — | Required. `blm_embed_…`. Never put a secret API key (`blm_live_…`) in a browser. |
|
|
95
|
+
| `apiBase` | `string` | the bundle's origin | Pass it explicitly when installing from npm. |
|
|
96
|
+
| `storage` | `'local' \| 'session' \| 'memory'` | `'local'` | `'local'` survives reloads; `'memory'` keeps nothing on disk. Every storage access is already guarded, so private windows and blocked site data degrade to memory rather than throwing. |
|
|
97
|
+
| `mode` | `'auto' \| 'element' \| 'iframe'` | `'auto'` | See [Rendering modes](#rendering-modes). |
|
|
98
|
+
| `theme` | `Record<string, string>` | `{}` | CSS custom properties, applied to each element's shadow root. Only `--…` names are accepted. |
|
|
99
|
+
|
|
100
|
+
### Session shape
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
{
|
|
104
|
+
token: string; // access token — the elements send it for you
|
|
105
|
+
refreshToken: string;
|
|
106
|
+
expiresAt: number; // epoch ms
|
|
107
|
+
user: { id, name, email, avatar: string | null };
|
|
108
|
+
project: { id, name };
|
|
109
|
+
grantedKeys: string[]; // feature keys, already narrowed to your token's ceiling
|
|
110
|
+
isProjectOwner: boolean;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Refresh is automatic: the SDK refreshes stale access tokens and retries a 401 once, and
|
|
115
|
+
concurrent calls share a single in-flight refresh. You never need to touch `refreshToken`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Events
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const off = BloomsEmbed.on('all', event => { /* … */ });
|
|
123
|
+
off(); // unsubscribe
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Event | Payload | When |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `session` | `{ type, session: EmbedSessionState \| null, mode }` | Sign-in, refresh, sign-out, expiry. `session: null` means signed out — show `<blooms-login>` again. |
|
|
129
|
+
| `error` | `{ type, code, message, status? }` | Any surfaced failure. Branch on `code`, not on `message`. |
|
|
130
|
+
| `navigate` | `{ type, feature, view, params }` | A component moved between its own views. Ignore it, or mirror it in your own URL. |
|
|
131
|
+
|
|
132
|
+
`<blooms-login>` also emits DOM events, so a host that never touches the SDK object still works:
|
|
133
|
+
`bloomssession` (`event.detail` is the session or `null`) and `bloomserror` (`event.detail` is the
|
|
134
|
+
error event above).
|
|
135
|
+
|
|
136
|
+
Codes you should actually handle:
|
|
137
|
+
|
|
138
|
+
| Code | What to do |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `EMBED_CREDENTIALS_INVALID` | Nothing — the login element shows it. |
|
|
141
|
+
| `EMBED_NO_PROJECT_ACCESS` | The account is valid but has no access to this project. Point the user at whoever administers their blooms.ai access. |
|
|
142
|
+
| `EMBED_ORIGIN_REJECTED` | Your origin is not registered on the token. Contact blooms.ai. |
|
|
143
|
+
| `EMBED_TOKEN_REVOKED` / `EMBED_PROJECT_INACTIVE` | Hide the integration; retrying will not help. |
|
|
144
|
+
| `EMBED_SESSION_EXPIRED` / `EMBED_SESSION_REVOKED` | Show sign-in again. |
|
|
145
|
+
| `EMBED_RATE_LIMITED` | Back off; `Retry-After` is honoured internally for requests. |
|
|
146
|
+
| `EMBED_FEATURE_DISABLED` | The feature is not enabled on your integration. |
|
|
147
|
+
| `EMBED_NETWORK` | Unreachable API base — very often an unregistered origin (see above). |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## `<blooms-login>`
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<blooms-login heading="Sign in to Acme Insights"></blooms-login>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Property | Attribute | Type | Purpose |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| `heading` | `heading` | `string` | Overrides the heading copy. |
|
|
160
|
+
| `subheading` | `subheading` | `string` | Overrides the default "Continue to *project*". |
|
|
161
|
+
| `compact` | `compact` | `boolean` | Drops the card chrome, for dropping into a panel you already styled. |
|
|
162
|
+
|
|
163
|
+
Attributes arrive as strings, so `compact="false"` is truthy. For booleans set the property
|
|
164
|
+
(`el.compact = true`) or leave the attribute off entirely.
|
|
165
|
+
|
|
166
|
+
The element handles its own states: loading skeleton, signed-out form, signed-in summary, and
|
|
167
|
+
distinct messages for a wrong password, an account without access to the project, a deactivated
|
|
168
|
+
or banned account, and rate limiting.
|
|
169
|
+
|
|
170
|
+
Google sign-in is not offered. Accounts created with Google must set a password in the blooms.ai
|
|
171
|
+
portal before they can use this form.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Styles and theming
|
|
176
|
+
|
|
177
|
+
`<blooms-login>` is fully self-styled: it renders correctly with no stylesheet of yours and no
|
|
178
|
+
extra request. Every element uses shadow DOM, so your CSS cannot reach in and ours cannot leak
|
|
179
|
+
out.
|
|
180
|
+
|
|
181
|
+
Feature components additionally use the shared stylesheet `blooms-embed.css`. At runtime the
|
|
182
|
+
bundle fetches it from beside itself — `<the bundle's own directory>/blooms-embed.css`, which is
|
|
183
|
+
the CDN in production. This package ships an identical
|
|
184
|
+
copy so you can serve it from your own app instead — required if you must work offline, or if
|
|
185
|
+
your CSP will not allow the fetch:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import '@bloomscorp/blooms-ai-embed/blooms-embed.css'; // if your bundler handles CSS imports
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<!-- or copy it out of node_modules and serve it yourself -->
|
|
193
|
+
<link rel="stylesheet" href="/assets/blooms-embed.css" />
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Theme with CSS custom properties — either through `configure({ theme })` or on the element itself.
|
|
197
|
+
These are read with fallbacks, so set only what you care about:
|
|
198
|
+
|
|
199
|
+
| Property | Default |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `--brand-primary` | `#5b3df6` (the accent: buttons, focus rings, links) |
|
|
202
|
+
| `--color-card-bg` | `#ffffff` light / `#1a1825` dark |
|
|
203
|
+
| `--color-text` | `#14121f` light / `#f4f3f8` dark |
|
|
204
|
+
| `--color-text-muted` | `#6b6880` light / `#a09db4` dark |
|
|
205
|
+
| `--color-border` | `#e7e5ee` light / `#2e2b3d` dark |
|
|
206
|
+
| `--radius-lg` | `16px` |
|
|
207
|
+
| `--shadow-md` | `0 8px 28px rgba(20, 18, 31, .10)` |
|
|
208
|
+
| `--font-sans` | system UI stack |
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
BloomsEmbed.configure({
|
|
212
|
+
token: 'blm_embed_…',
|
|
213
|
+
apiBase: 'https://api.blooms.ai',
|
|
214
|
+
theme: { '--brand-primary': '#0f766e', '--radius-lg': '10px' },
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Dark mode follows the host's `prefers-color-scheme` unless you set the properties explicitly.
|
|
219
|
+
Your blooms.ai admin can also set a default theme on the token; `configure({ theme })` wins over it.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Framework notes
|
|
224
|
+
|
|
225
|
+
### Plain HTML / Vue / Svelte / Solid
|
|
226
|
+
|
|
227
|
+
Nothing special. Import the package once at startup and use the tags.
|
|
228
|
+
|
|
229
|
+
### React 19+
|
|
230
|
+
|
|
231
|
+
Works natively — React 19 sets unknown props as *properties* on custom elements, so object and
|
|
232
|
+
boolean props arrive intact. For events, subscribe through the SDK (the same code works in every
|
|
233
|
+
React version):
|
|
234
|
+
|
|
235
|
+
```jsx
|
|
236
|
+
import { useEffect } from 'react';
|
|
237
|
+
import { BloomsEmbed } from '@bloomscorp/blooms-ai-embed';
|
|
238
|
+
|
|
239
|
+
BloomsEmbed.configure({ token: 'blm_embed_…', apiBase: 'https://api.blooms.ai' });
|
|
240
|
+
|
|
241
|
+
export function Login({ onSession }) {
|
|
242
|
+
useEffect(() => BloomsEmbed.on('session', e => onSession(e.session)), [onSession]);
|
|
243
|
+
return <blooms-login heading="Sign in" />;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### React <= 18
|
|
248
|
+
|
|
249
|
+
React 17/18 write every JSX prop as an **attribute** (strings only) and do not attach listeners
|
|
250
|
+
for lowercase custom events. Wrap the element once — properties via a ref, events via
|
|
251
|
+
`addEventListener`:
|
|
252
|
+
|
|
253
|
+
```jsx
|
|
254
|
+
import { useEffect, useRef } from 'react';
|
|
255
|
+
import '@bloomscorp/blooms-ai-embed';
|
|
256
|
+
|
|
257
|
+
export function BloomsLogin({ heading, subheading, compact = false, onSession, onError }) {
|
|
258
|
+
const ref = useRef(null);
|
|
259
|
+
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const el = ref.current;
|
|
262
|
+
if (!el) return;
|
|
263
|
+
el.heading = heading ?? ''; // properties, not attributes
|
|
264
|
+
el.subheading = subheading ?? '';
|
|
265
|
+
el.compact = compact;
|
|
266
|
+
}, [heading, subheading, compact]);
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
const el = ref.current;
|
|
270
|
+
if (!el) return;
|
|
271
|
+
const session = e => onSession?.(e.detail);
|
|
272
|
+
const error = e => onError?.(e.detail);
|
|
273
|
+
el.addEventListener('bloomssession', session);
|
|
274
|
+
el.addEventListener('bloomserror', error);
|
|
275
|
+
return () => {
|
|
276
|
+
el.removeEventListener('bloomssession', session);
|
|
277
|
+
el.removeEventListener('bloomserror', error);
|
|
278
|
+
};
|
|
279
|
+
}, [onSession, onError]);
|
|
280
|
+
|
|
281
|
+
return <blooms-login ref={ref} />;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Angular host
|
|
286
|
+
|
|
287
|
+
Add `CUSTOM_ELEMENTS_SCHEMA` to the component or module that uses the tags, or Angular's
|
|
288
|
+
compiler rejects the unknown element and its properties:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
@Component({
|
|
292
|
+
selector: 'app-insights',
|
|
293
|
+
standalone: true,
|
|
294
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
295
|
+
template: `<blooms-login [compact]="true" (bloomssession)="onSession($event)"></blooms-login>`,
|
|
296
|
+
})
|
|
297
|
+
export class InsightsComponent {}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The embed bundle is zoneless and boots its own Angular context, so it does not interfere with
|
|
301
|
+
your app's zone, router or DI — but include it **once** per page. Loading it twice (say, the npm
|
|
302
|
+
package plus a `<script>` tag) is detected and the second copy yields with a console warning,
|
|
303
|
+
which is a warning you should fix rather than live with.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Electron
|
|
308
|
+
|
|
309
|
+
Element mode works in an Electron renderer as long as the renderer has a real, sendable origin.
|
|
310
|
+
Get a `clientType: 'desktop'` token from blooms.ai and register that origin on it.
|
|
311
|
+
|
|
312
|
+
**Install this package and bundle it.** Do not load the bundle over `<script src="https://…">`
|
|
313
|
+
into a renderer: remote code in a renderer is exactly what Electron's security checklist tells
|
|
314
|
+
you to avoid, and a bundled copy also works offline.
|
|
315
|
+
|
|
316
|
+
**Custom scheme (`app://`) — recommended.** Register the scheme as privileged before any window
|
|
317
|
+
loads, in the main process:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
const { app, protocol, BrowserWindow } = require('electron');
|
|
321
|
+
|
|
322
|
+
protocol.registerSchemesAsPrivileged([{
|
|
323
|
+
scheme: 'app',
|
|
324
|
+
privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true },
|
|
325
|
+
}]);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Without `standard` + `secure` the renderer has an opaque origin, `fetch` is not available to it,
|
|
329
|
+
and the API will never authorize it. Then ask blooms.ai to register `app://your-app-name` on the
|
|
330
|
+
token.
|
|
331
|
+
|
|
332
|
+
**`http://localhost` renderers** work too — register the exact origin including the port.
|
|
333
|
+
|
|
334
|
+
**Never set `webSecurity: false`.** It is the usual "fix" for this class of problem and it
|
|
335
|
+
disables the protections that make embedding safe at all. If requests are being refused, the
|
|
336
|
+
cause is an unregistered origin or a missing privileged-scheme registration, not web security.
|
|
337
|
+
|
|
338
|
+
**CSP.** Your renderer's CSP must allow calls to the API and the loading of the stylesheet:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
default-src 'self' app:;
|
|
342
|
+
connect-src 'self' https://api.blooms.ai;
|
|
343
|
+
style-src 'self' 'unsafe-inline';
|
|
344
|
+
img-src 'self' data: https:;
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`connect-src` must name your `apiBase` exactly. `style-src 'unsafe-inline'` is needed because the
|
|
348
|
+
components inject their styles into shadow roots; if your policy forbids it, serve
|
|
349
|
+
`blooms-embed.css` from your own app and use a nonce-free `<link>` for it.
|
|
350
|
+
|
|
351
|
+
**`file://` renderers cannot use element mode.** A `file://` page has an opaque origin, so the
|
|
352
|
+
server has nothing to authorize, and the SDK falls back to the iframe automatically. That path
|
|
353
|
+
needs `allowFraming` and `allowOpaqueAncestor` set on your token by a blooms.ai admin — a
|
|
354
|
+
deliberate opt-in, because it means the frame cannot restrict who embeds it. Prefer a custom
|
|
355
|
+
scheme or `http://localhost` and keep element mode.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Rendering modes
|
|
360
|
+
|
|
361
|
+
`configure({ mode: 'auto' })` is the default and picks:
|
|
362
|
+
|
|
363
|
+
| Situation | Mode |
|
|
364
|
+
|---|---|
|
|
365
|
+
| A normal page on a real origin (`https://…`, `http://localhost`, privileged `app://`) | `element` |
|
|
366
|
+
| `file://`, an opaque origin, or no `customElements` | `iframe` |
|
|
367
|
+
|
|
368
|
+
An explicit `mode` always wins. The resolved mode is reported on every `session` event and by
|
|
369
|
+
`BloomsEmbed.mode()`, so you can log which path a given install got. Both modes run the same
|
|
370
|
+
compiled components; the iframe exists so hosts that cannot present a registrable origin still
|
|
371
|
+
work.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## TypeScript
|
|
376
|
+
|
|
377
|
+
Types ship with the package — no `@types` install. The declarations also register
|
|
378
|
+
`<blooms-login>` in `HTMLElementTagNameMap`, so `document.querySelector('blooms-login')` is typed,
|
|
379
|
+
and add `BloomsEmbed` to `Window`.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { BloomsEmbed, type EmbedSessionState } from '@bloomscorp/blooms-ai-embed';
|
|
383
|
+
|
|
384
|
+
BloomsEmbed.configure({ token: 'blm_embed_…', apiBase: 'https://api.blooms.ai' });
|
|
385
|
+
|
|
386
|
+
const session: EmbedSessionState | null = await BloomsEmbed.resume();
|
|
387
|
+
if (session && BloomsEmbed.can('analytics')) { /* … */ }
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Exported types: `BloomsEmbedConfig`, `ResolvedEmbedConfig`, `EmbedMode`, `EmbedModeSetting`,
|
|
391
|
+
`EmbedStorage`, `EmbedUser`, `EmbedProject`, `EmbedSessionState`, `EmbedEvent` (plus
|
|
392
|
+
`EmbedSessionEvent`, `EmbedErrorEvent`, `EmbedNavigateEvent`), `EmbedManifest`, `BloomsEmbedSdk`,
|
|
393
|
+
`BloomsLoginElement`.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Server-side rendering
|
|
398
|
+
|
|
399
|
+
The package touches the DOM as it loads, so importing it on a server throws a deliberate,
|
|
400
|
+
explanatory error rather than a confusing `ReferenceError`. Import it lazily on the client:
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
useEffect(() => { import('@bloomscorp/blooms-ai-embed').then(({ BloomsEmbed }) => BloomsEmbed.configure({ /* … */ })); }, []);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
In Electron, that also means: import it in the **renderer**, never in the main process.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Security notes worth reading once
|
|
411
|
+
|
|
412
|
+
- The embed token is publishable, but it is not a secret to be careless with: it is what ties
|
|
413
|
+
usage back to your integration, and it is revocable. Secret API keys (`blm_live_…`) must never
|
|
414
|
+
reach a browser.
|
|
415
|
+
- Your users type blooms.ai credentials into a page you control. Shadow DOM is encapsulation, not
|
|
416
|
+
a security boundary — a compromised host page can read them. This is why embed tokens are
|
|
417
|
+
issued by blooms.ai admins to partners under contract rather than self-service.
|
|
418
|
+
- `BloomsEmbed.can()` is a UI convenience. The server re-authorizes every single request against
|
|
419
|
+
the user's live permissions, so a stale client-side `true` gets you a 403, not data.
|
|
420
|
+
- Every sign-in, refusal, refresh and feature action is logged against your integration and
|
|
421
|
+
visible to blooms.ai admins and to the project's owner.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Support
|
|
426
|
+
|
|
427
|
+
Questions, origin registration, feature enablement and token rotation all go through your
|
|
428
|
+
blooms.ai contact. Include your project name and the integration name from the token.
|
|
429
|
+
|
|
430
|
+
## Multiple projects
|
|
431
|
+
|
|
432
|
+
An integration can be scoped to several projects. The user signs in **once** and
|
|
433
|
+
switching is instant — no round trip, no re-authentication.
|
|
434
|
+
|
|
435
|
+
```js
|
|
436
|
+
const projects = BloomsEmbed.projects(); // [{ id, name, grantedKeys, isOwner }, …]
|
|
437
|
+
BloomsEmbed.setProject(projects[1].id); // returns false if not permitted
|
|
438
|
+
BloomsEmbed.project(); // the active one
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Grants differ per project, so ask per project:
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
BloomsEmbed.can('keywords'); // the ACTIVE project
|
|
445
|
+
BloomsEmbed.can('keywords', someOtherId); // a specific one
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`can()` returns `false` for a project the session cannot act on — it does not
|
|
449
|
+
fall back to the active project's permissions.
|
|
450
|
+
|
|
451
|
+
`setProject()` emits a `session` event, so a listener added with
|
|
452
|
+
`BloomsEmbed.on('session', …)` re-renders on a switch without a separate
|
|
453
|
+
subscription.
|
|
454
|
+
|
|
455
|
+
A single-project integration reports one entry, and every field you already use
|
|
456
|
+
keeps its meaning — it describes the active project.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@charset "UTF-8";@font-face{font-family:Poppins;font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDz8Z11lFc-K.woff2) format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Poppins;font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Poppins;font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Poppins;font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiEyp8kv8JHgFVrJJbecmNE.woff2) format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Poppins;font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Poppins;font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiEyp8kv8JHgFVrJJfecg.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Poppins;font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLGT9Z11lFc-K.woff2) format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Poppins;font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Poppins;font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Poppins;font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Poppins;font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Poppins;font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Poppins;font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2) format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Poppins;font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Poppins;font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Poppins;font-style:normal;font-weight:800;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDD4Z11lFc-K.woff2) format("woff2");unicode-range:U+0900-097F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Poppins;font-style:normal;font-weight:800;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDD4Z1JlFc-K.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Poppins;font-style:normal;font-weight:800;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDD4Z1xlFQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--color-primary-soft: #E3D8F1;--color-secondary: #DABECA;--color-accent: #E84855;--color-accent-soft: #f5c5c9;--color-text: #5D5F71;--color-text-muted: #9395a5;--color-bg: #f7f5fb;--color-card-bg: #FFFFFF;--color-sidebar-bg: #FFFFFF;--color-rail-bg: #f3f0f8;--color-border: #ede9f6;--color-hover: #f5f2fb;--font-sans: "Poppins", system-ui, -apple-system, sans-serif;--radius-sm: 6px;--radius-md: 10px;--radius-lg: 16px;--radius-full: 9999px;--shadow-sm: 0 1px 4px rgba(0, 0, 0, .06);--shadow-md: 0 4px 16px rgba(0, 0, 0, .08);--shadow-lg: 0 8px 32px rgba(0, 0, 0, .1);--z-agent-button: 9000;--z-agent-widget: 9100;--z-agent-menu: 9200;--z-agent-banner: 9300}@keyframes agent-halo-spin{to{transform:rotate(360deg)}}@keyframes agent-halo-pulse{0%,to{opacity:.8}50%{opacity:1}}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-text-size-adjust:100%;scroll-behavior:smooth}body{font-family:var(--font-sans);font-size:14px;color:var(--color-text);background-color:var(--color-bg);line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bui-tooltip{position:fixed;z-index:100000;max-width:260px;padding:7px 11px;border-radius:12px;background:#fff;color:#1d1d1f;border:1px solid rgba(0,0,0,.06);box-shadow:0 8px 26px #00000029,0 2px 6px #0000001a;font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);font-size:12.5px;font-weight:500;line-height:1.45;letter-spacing:.01em;text-align:left;white-space:normal;word-break:break-word;pointer-events:none;opacity:0;transform:scale(.92);transform-origin:center;transition:opacity .13s ease,transform .14s cubic-bezier(.34,1.56,.64,1)}.bui-tooltip--in{opacity:1;transform:scale(1)}.bui-tooltip__tail{position:absolute;z-index:-1;width:9px;height:9px;background:#fff;border:1px solid rgba(0,0,0,.06);border-radius:2px;transform:rotate(45deg)}.bui-tooltip__tail.is-bottom{bottom:-5px}.bui-tooltip__tail.is-top{top:-5px}.bui-tooltip__tail.is-right{right:-5px}.bui-tooltip__tail.is-left{left:-5px}@media (prefers-reduced-motion: reduce){.bui-tooltip{transition:opacity .1s ease;transform:none}.bui-tooltip--in{transform:none}}h1,h2,h3,h4,h5,h6{font-family:var(--font-sans);font-weight:700;line-height:1.3;color:var(--color-text)}a{color:var(--color-accent);text-decoration:none}a:hover{text-decoration:underline}p{line-height:1.7}.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;border-radius:var(--radius-md);font-family:var(--font-sans);font-size:13px;font-weight:600;cursor:pointer;border:1px solid transparent;transition:background .15s,border-color .15s,box-shadow .15s,opacity .15s;text-decoration:none;white-space:nowrap}.btn:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.btn-primary{background:var(--color-accent);color:#fff;border-color:var(--color-accent)}.btn-primary:hover{background:#d13d4a;border-color:#d13d4a;box-shadow:0 2px 8px #e848554d;text-decoration:none}.btn-outline{background:transparent;color:var(--color-text);border-color:var(--color-border)}.btn-outline:hover{background:var(--color-hover);border-color:#d5d0e4;text-decoration:none}.btn-ghost{background:transparent;color:var(--color-text-muted);border-color:transparent}.btn-ghost:hover{background:var(--color-hover);color:var(--color-text)}.btn-sm{padding:6px 12px;font-size:12px;border-radius:var(--radius-sm)}.btn-lg{padding:12px 24px;font-size:15px;border-radius:12px}input,textarea,select{font-family:var(--font-sans);font-size:14px;color:var(--color-text)}.text-muted{color:var(--color-text-muted)}.text-accent{color:var(--color-accent)}.text-sm{font-size:12px}.text-xs{font-size:11px}.font-semibold{font-weight:600}.font-bold{font-weight:700}.flex{display:flex}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:8px}.gap-4{gap:16px}.w-full{width:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--color-border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--color-secondary)}router-outlet+*{animation:fadeInUp .2s ease}@keyframes fadeInUp{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.pane-section{display:flex;flex-direction:column;gap:24px;max-width:520px}.pane-hd{display:flex;flex-direction:column;gap:4px;padding-bottom:20px;border-bottom:1px solid var(--color-border)}.pane-hd--row{flex-direction:row;align-items:flex-start;justify-content:space-between;gap:16px}.pane-title{font-size:15px;font-weight:700;color:var(--color-text);margin:0}.pane-desc{font-size:12px;color:var(--color-text-muted);margin:0;line-height:1.5}.pane-form{display:flex;flex-direction:column;gap:18px}.pane-success{font-size:12px;font-weight:500;color:#065f46;background:#d1fae5;border:1px solid #a7f3d0;border-radius:var(--radius-sm);padding:8px 12px;margin:0}.field{display:flex;flex-direction:column;gap:6px}.field__label{font-size:12px;font-weight:600;color:var(--color-text);letter-spacing:.02em;text-transform:uppercase}.field__input{padding:9px 13px;border:1px solid var(--color-border);border-radius:var(--radius-md);font-family:var(--font-sans);font-size:13px;color:var(--color-text);background:var(--color-bg);outline:none;transition:border-color .15s,box-shadow .15s;width:100%}.field__input:focus{border-color:var(--color-accent);box-shadow:0 0 0 3px #e8485514}.field__input--locked{opacity:.55;cursor:not-allowed;color:var(--color-text-muted)}.field__input:disabled{cursor:not-allowed}.field textarea.field__input{resize:vertical}.field__hint{font-size:11px;color:var(--color-text-muted)}@media (max-width: 768px){.btn{padding:8px 14px}.detail-table-wrap,.pages-table-wrap,.diff-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}.result-summary,.insight-grid,.pr-stats-grid{grid-template-columns:1fr 1fr!important}.sitemap-form{flex-direction:column}.pr-detail__cols,.diff-screen__body{grid-template-columns:1fr!important}.diff-files{display:none}.diff-files.mobile-show{display:block}.pr-filters__top{flex-direction:column;align-items:flex-start}.pr-filters__right{width:100%}.pr-search{max-width:100%!important;flex:1}.repo-card{flex-direction:column;align-items:flex-start}.repo-card__right{align-self:flex-start;flex-wrap:wrap}.history-item{flex-direction:column}.results-toolbar{flex-direction:column;align-items:flex-start}.diff-screen__title-row{flex-direction:column}.diff-screen__header-right{align-self:flex-start}}
|