@nominalso/vibe-host 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -11
- package/dist/index.cjs +213 -52
- package/dist/index.d.cts +194 -12
- package/dist/index.d.ts +194 -12
- package/dist/index.js +211 -52
- package/package.json +1 -11
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# @nominalso/vibe-host
|
|
2
2
|
|
|
3
|
-
Host-side SDK for embedding **Nominal Vibe Apps**. Used by the Nominal app (nom-ui) to receive requests from an
|
|
4
|
-
|
|
5
|
-
**
|
|
6
|
-
pointing at the Nominal API, and the SDK wires up and dispatches all operations for you.
|
|
3
|
+
Host-side SDK for embedding **Nominal Vibe Apps**. Used by the Nominal app (nom-ui) to receive requests from an embedded Vibe App over a typed `postMessage` protocol and dispatch them to Nominal APIs. The host ships a **complete, type-checked handler map** covering every protocol operation — you supply a `clientConfig` pointing at the Nominal API, and the SDK wires up and dispatches all operations for you.
|
|
4
|
+
|
|
5
|
+
> **For AI agents:** you do **not** pass a `handlers` map. Provide `clientConfig` (where the Nominal API lives) and the SDK builds every handler. The iframe-side counterpart is `@nominalso/vibe-bridge`.
|
|
7
6
|
|
|
8
7
|
## Install
|
|
9
8
|
|
|
@@ -11,28 +10,93 @@ pointing at the Nominal API, and the SDK wires up and dispatches all operations
|
|
|
11
10
|
npm install @nominalso/vibe-host
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
Externalizes only `@hey-api/client-fetch`; TypeScript types are self-contained.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
15
16
|
|
|
16
17
|
```tsx
|
|
17
18
|
import { VibeAppHost } from '@nominalso/vibe-host'
|
|
18
19
|
|
|
19
20
|
const host = new VibeAppHost({
|
|
21
|
+
// Iframe origin(s) allowed to talk to this host — must match exactly.
|
|
20
22
|
trustedOrigins: ['https://my-vibe-app.lovable.app'],
|
|
23
|
+
// This Vibe App's base path in nom-ui (used to sync the browser URL).
|
|
21
24
|
appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
25
|
+
// Context pushed to the iframe on connect.
|
|
22
26
|
getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
|
|
23
27
|
// Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy route).
|
|
24
|
-
// The SDK provides handlers for every protocol operation out of the box.
|
|
25
28
|
clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
|
|
26
29
|
})
|
|
27
30
|
|
|
28
|
-
// Start listening
|
|
31
|
+
// Start listening BEFORE the iframe loads, then push context on its `load` event.
|
|
29
32
|
const unmount = host.mount()
|
|
30
|
-
// host.pushContextTo(iframe.contentWindow)
|
|
33
|
+
// once the iframe has loaded: host.pushContextTo(iframe.contentWindow)
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### `new VibeAppHost(options)`
|
|
39
|
+
|
|
40
|
+
| Option | Type | Description |
|
|
41
|
+
| ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `trustedOrigins` | `string[]` | Iframe origins allowed to talk to this host. Messages from other origins are ignored. |
|
|
43
|
+
| `getContext` | `() => HostContext` | Returns the current context to send to the iframe (`hostVersion` is injected automatically). |
|
|
44
|
+
| `appBasePath` | `string` | Base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`. |
|
|
45
|
+
| `clientConfig` | `VibeApiClientOptions?` | Points the built-in handler map at the Nominal API. Defaults to `{ baseUrl: '', stripApiPrefix: false }`. |
|
|
46
|
+
|
|
47
|
+
`VibeApiClientOptions` is `{ baseUrl: string; stripApiPrefix?: boolean }`. Set `stripApiPrefix: true` when routing through a proxy whose convention expects paths without the `/api` prefix.
|
|
48
|
+
|
|
49
|
+
### `mount(iframeWindow?): () => void`
|
|
50
|
+
|
|
51
|
+
Starts listening for bridge messages and sets up browser back/forward sync. Returns a cleanup function. Pass `iframeWindow` to immediately push context; if omitted, the host answers `GET_CONTEXT` polls but won't proactively push until you call `pushContextTo`.
|
|
52
|
+
|
|
53
|
+
### `pushContextTo(iframeWindow): void`
|
|
54
|
+
|
|
55
|
+
Pushes the current context to the iframe and stores the window reference for future pushes (e.g. subroute sync). Call once the iframe has loaded if you called `mount()` with no argument.
|
|
56
|
+
|
|
57
|
+
### Exports
|
|
58
|
+
|
|
59
|
+
`VibeAppHost`, and the types `VibeAppHostOptions`, `HostContext`, `HostHandlers`, `RequestHandlers`, `ContextPayload`, `VibeApiClientOptions`.
|
|
60
|
+
|
|
61
|
+
## Mounting inside Next.js (nom-ui)
|
|
62
|
+
|
|
63
|
+
The iframe is server-rendered, so it may start loading before React hydrates, and cross-origin `contentDocument` is always `null`. The robust pattern: `mount()` (no arg) immediately to start listening, then `pushContextTo(iframe.contentWindow)` from the iframe's `load` event.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
'use client'
|
|
67
|
+
import { useEffect, useRef } from 'react'
|
|
68
|
+
import { VibeAppHost } from '@nominalso/vibe-host'
|
|
69
|
+
|
|
70
|
+
function VibeAppFrame({ host, src }: { host: VibeAppHost; src: string }) {
|
|
71
|
+
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const iframe = iframeRef.current
|
|
75
|
+
if (!iframe) return
|
|
76
|
+
const unmount = host.mount() // listen before the iframe loads
|
|
77
|
+
const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
|
|
78
|
+
iframe.addEventListener('load', onLoad)
|
|
79
|
+
return () => {
|
|
80
|
+
iframe.removeEventListener('load', onLoad)
|
|
81
|
+
unmount()
|
|
82
|
+
}
|
|
83
|
+
}, [host])
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<iframe
|
|
87
|
+
ref={iframeRef}
|
|
88
|
+
src={src}
|
|
89
|
+
sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Calling `contentWindow.postMessage` while the iframe is still at `about:blank` throws a `DOMException` — that's why context is pushed from the `load` event, not on effect setup.
|
|
96
|
+
|
|
97
|
+
## How it fits together
|
|
98
|
+
|
|
99
|
+
The iframe side is [`@nominalso/vibe-bridge`](https://www.npmjs.com/package/@nominalso/vibe-bridge). See the [repository](https://github.com/nominalso/vibe-apps-sdk#readme) for the full protocol and architecture.
|
|
36
100
|
|
|
37
101
|
## License
|
|
38
102
|
|
package/dist/index.cjs
CHANGED
|
@@ -20,14 +20,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BridgeError: () => BridgeError,
|
|
24
|
+
HttpBridgeError: () => HttpBridgeError,
|
|
23
25
|
VibeAppHost: () => VibeAppHost
|
|
24
26
|
});
|
|
25
27
|
module.exports = __toCommonJS(index_exports);
|
|
26
28
|
|
|
27
29
|
// package.json
|
|
28
|
-
var version = "0.0
|
|
30
|
+
var version = "0.2.0";
|
|
29
31
|
|
|
30
32
|
// ../protocol-types/dist/index.js
|
|
33
|
+
function normalizeSubroute(subroute) {
|
|
34
|
+
const normalized = subroute.startsWith("/") ? subroute : `/${subroute}`;
|
|
35
|
+
const path = normalized.split(/[?#]/)[0];
|
|
36
|
+
let decoded = path;
|
|
37
|
+
let prev;
|
|
38
|
+
do {
|
|
39
|
+
prev = decoded;
|
|
40
|
+
decoded = decoded.replace(/%25/gi, "%").replace(/%2e/gi, ".").replace(/%2f/gi, "/");
|
|
41
|
+
} while (decoded !== prev);
|
|
42
|
+
if (decoded.split("/").some((segment) => segment === "..")) return null;
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
31
45
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
32
46
|
var PROTOCOL_VERSION = 1;
|
|
33
47
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
@@ -44,6 +58,20 @@ function isBridgeMessage(data) {
|
|
|
44
58
|
if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
|
|
45
59
|
return true;
|
|
46
60
|
}
|
|
61
|
+
var BridgeError = class extends Error {
|
|
62
|
+
constructor(code, message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.name = "BridgeError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var HttpBridgeError = class extends BridgeError {
|
|
69
|
+
constructor(status, message) {
|
|
70
|
+
super("REQUEST_FAILED", message);
|
|
71
|
+
this.status = status;
|
|
72
|
+
this.name = "HttpBridgeError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
47
75
|
|
|
48
76
|
// ../api-client/dist/index.js
|
|
49
77
|
var import_client_fetch = require("@hey-api/client-fetch");
|
|
@@ -375,10 +403,6 @@ var getSubsidiaryParentCurrenciesApiTenancySubsidiarySubsidiaryIdParentCurrencie
|
|
|
375
403
|
};
|
|
376
404
|
function createVibeApiClient({ baseUrl, stripApiPrefix }) {
|
|
377
405
|
const client2 = (0, import_client_fetch2.createClient)((0, import_client_fetch2.createConfig)({ baseUrl }));
|
|
378
|
-
client2.interceptors.request.use((request) => {
|
|
379
|
-
console.log("[vibe-api-client] request url:", request.url);
|
|
380
|
-
return request;
|
|
381
|
-
});
|
|
382
406
|
if (stripApiPrefix) {
|
|
383
407
|
client2.interceptors.request.use(
|
|
384
408
|
(request) => new Request(request.url.replace(`${baseUrl}/api/`, `${baseUrl}/`), request)
|
|
@@ -387,16 +411,28 @@ function createVibeApiClient({ baseUrl, stripApiPrefix }) {
|
|
|
387
411
|
return client2;
|
|
388
412
|
}
|
|
389
413
|
|
|
390
|
-
// src/
|
|
414
|
+
// src/handlers/call.ts
|
|
391
415
|
async function call(fn, payload, client2) {
|
|
392
|
-
const { data } = await fn({ ...payload, client: client2 });
|
|
393
|
-
if (data == null)
|
|
416
|
+
const { data, response } = await fn({ ...payload, client: client2 });
|
|
417
|
+
if (data == null) {
|
|
418
|
+
if (response && !response.ok) {
|
|
419
|
+
throw new HttpBridgeError(
|
|
420
|
+
response.status,
|
|
421
|
+
`API request failed with status ${response.status}`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (!response) {
|
|
425
|
+
throw new Error("API request produced no response");
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
394
429
|
return data;
|
|
395
430
|
}
|
|
396
|
-
|
|
397
|
-
|
|
431
|
+
|
|
432
|
+
// src/handlers/accounting.ts
|
|
433
|
+
function accountingHandlers(client2) {
|
|
398
434
|
return {
|
|
399
|
-
//
|
|
435
|
+
// Chart of accounts
|
|
400
436
|
GET_CHART_OF_ACCOUNTS: (p) => call(getCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGet, p, client2),
|
|
401
437
|
GET_COA_TREE: (p) => call(getCoaTreeApiAccountingSubsidiarySubsidiaryIdCoaGet, p, client2),
|
|
402
438
|
GET_COA_FLAT_SIMPLE: (p) => call(
|
|
@@ -421,7 +457,7 @@ function createHandlerMap(options) {
|
|
|
421
457
|
),
|
|
422
458
|
GET_ACCOUNTS: (p) => call(getAccountsApiAccountingAccountGet, p, client2),
|
|
423
459
|
GET_ACCOUNT: (p) => call(getAccountApiAccountingAccountAccountIdGet, p, client2),
|
|
424
|
-
//
|
|
460
|
+
// Exchange rates
|
|
425
461
|
GET_CONVERSION_RATES: (p) => call(getConversionRateApiAccountingCurrencyCurrencyConversionRatesGet, p, client2),
|
|
426
462
|
GET_EFFECTIVE_EXCHANGE_RATE: (p) => call(getEffectiveExchangeRateApiAccountingConsolidationExchangeRateGet, p, client2),
|
|
427
463
|
GET_EXCHANGE_RATE_BY_DATE: (p) => call(
|
|
@@ -429,7 +465,7 @@ function createHandlerMap(options) {
|
|
|
429
465
|
p,
|
|
430
466
|
client2
|
|
431
467
|
),
|
|
432
|
-
//
|
|
468
|
+
// Dimensions
|
|
433
469
|
GET_DIMENSIONS: (p) => call(getDimensionsApiAccountingDimensionGet, p, client2),
|
|
434
470
|
GET_DIMENSION_VALUES: (p) => call(getDimensionValuesApiAccountingDimensionValueGet, p, client2),
|
|
435
471
|
GET_DIMENSION_VALUES_HIERARCHICAL: (p) => call(
|
|
@@ -442,7 +478,7 @@ function createHandlerMap(options) {
|
|
|
442
478
|
p,
|
|
443
479
|
client2
|
|
444
480
|
),
|
|
445
|
-
//
|
|
481
|
+
// Journal entries
|
|
446
482
|
GET_JOURNAL_ENTRIES: (p) => call(
|
|
447
483
|
getSubsidiaryJournalApiAccountingSubsidiarySubsidiaryIdJournalEntryGet,
|
|
448
484
|
p,
|
|
@@ -457,8 +493,14 @@ function createHandlerMap(options) {
|
|
|
457
493
|
getJournalEntryApiAccountingSubsidiarySubsidiaryIdJournalEntryJournalEntryIdGet,
|
|
458
494
|
p,
|
|
459
495
|
client2
|
|
460
|
-
)
|
|
461
|
-
|
|
496
|
+
)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/handlers/activity.ts
|
|
501
|
+
function activityHandlers(client2) {
|
|
502
|
+
return {
|
|
503
|
+
// Period instances
|
|
462
504
|
GET_PERIODS: (p) => call(getPeriodInstancesApiActivityPeriodInstanceGet, p, client2),
|
|
463
505
|
GET_PERIOD_INSTANCE: (p) => call(getPeriodInstanceApiActivityPeriodInstancePeriodInstanceIdGet, p, client2),
|
|
464
506
|
GET_PERIOD_INSTANCE_BY_SLUG: (p) => call(
|
|
@@ -471,14 +513,14 @@ function createHandlerMap(options) {
|
|
|
471
513
|
p,
|
|
472
514
|
client2
|
|
473
515
|
),
|
|
474
|
-
// Activity
|
|
516
|
+
// Activity definitions
|
|
475
517
|
GET_ACTIVITY_DEFINITIONS: (p) => call(getActivityDefinitionsApiActivityActivityDefinitionGet, p, client2),
|
|
476
518
|
GET_ACTIVITY_DEFINITION: (p) => call(
|
|
477
519
|
getActivityDefinitionByIdApiActivityActivityDefinitionActivityDefinitionIdGet,
|
|
478
520
|
p,
|
|
479
521
|
client2
|
|
480
522
|
),
|
|
481
|
-
// Activity
|
|
523
|
+
// Activity instances
|
|
482
524
|
GET_ACTIVITY_INSTANCES: (p) => call(getActivityInstancesApiActivityActivityInstanceGet, p, client2),
|
|
483
525
|
GET_ACTIVITY_INSTANCE: (p) => call(getActivityInstanceApiActivityActivityInstanceActivityInstanceIdGet, p, client2),
|
|
484
526
|
GET_ACTIVITY_INSTANCE_BY_PERIOD: (p) => call(
|
|
@@ -496,28 +538,43 @@ function createHandlerMap(options) {
|
|
|
496
538
|
p,
|
|
497
539
|
client2
|
|
498
540
|
),
|
|
499
|
-
//
|
|
541
|
+
// Task definitions
|
|
500
542
|
GET_TASK_DEFINITIONS: (p) => call(getAllTasksDefinitionsApiActivityTaskDefinitionGet, p, client2),
|
|
501
543
|
GET_TASK_DEFINITION: (p) => call(getTaskDefinitionByIdApiActivityTaskDefinitionTaskDefinitionIdGet, p, client2),
|
|
502
544
|
CREATE_TASK_DEFINITION: (p) => call(createTaskDefinitionApiActivityTaskDefinitionPost, p, client2),
|
|
503
545
|
UPDATE_TASK_DEFINITION: (p) => call(updateTaskDefinitionApiActivityTaskDefinitionTaskDefinitionIdPut, p, client2),
|
|
504
546
|
GET_TASK_DEFINITIONS_BY_FILTER: (p) => call(getAllTasksDefinitionsByFilterApiActivityTaskDefinitionQueryPost, p, client2),
|
|
505
|
-
//
|
|
547
|
+
// Task instances
|
|
506
548
|
GET_TASK_INSTANCES: (p) => call(getAllTaskInstancesApiActivityTaskInstanceGet, p, client2),
|
|
507
549
|
GET_TASK_INSTANCE: (p) => call(getTaskInstanceByIdApiActivityTaskInstanceTaskInstanceIdGet, p, client2),
|
|
508
550
|
POST_TASK_OUTPUT: (p) => call(updateTaskInstanceApiActivityTaskInstanceTaskInstanceIdPut, p, client2),
|
|
509
|
-
GET_TASK_INSTANCES_BY_FILTER: (p) => call(getAllTaskInstancesByFilterApiActivityTaskInstanceQueryPost, p, client2)
|
|
510
|
-
|
|
551
|
+
GET_TASK_INSTANCES_BY_FILTER: (p) => call(getAllTaskInstancesByFilterApiActivityTaskInstanceQueryPost, p, client2)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/handlers/audit-trail.ts
|
|
556
|
+
function auditTrailHandlers(client2) {
|
|
557
|
+
return {
|
|
511
558
|
GET_AUDIT_EVENTS: (p) => call(getEventsApiAuditTrailEventGet, p, client2),
|
|
512
|
-
GET_ENTITY_AUDIT_EVENTS: (p) => call(getEntityEventsApiAuditTrailEventEntityTargetTypeTargetIdPost, p, client2)
|
|
513
|
-
|
|
559
|
+
GET_ENTITY_AUDIT_EVENTS: (p) => call(getEntityEventsApiAuditTrailEventEntityTargetTypeTargetIdPost, p, client2)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/handlers/period-manager.ts
|
|
564
|
+
function periodManagerHandlers(client2) {
|
|
565
|
+
return {
|
|
514
566
|
GET_FISCAL_CALENDARS: (p) => call(
|
|
515
567
|
getFiscalCalendarsBySubsidiaryApiPeriodManagerFiscalCalendarBySubsidiarySubsidiaryIdGet,
|
|
516
568
|
p,
|
|
517
569
|
client2
|
|
518
570
|
),
|
|
519
|
-
GET_FISCAL_CALENDAR: (p) => call(getFiscalCalendarByIdApiPeriodManagerFiscalCalendarCalendarIdGet, p, client2)
|
|
520
|
-
|
|
571
|
+
GET_FISCAL_CALENDAR: (p) => call(getFiscalCalendarByIdApiPeriodManagerFiscalCalendarCalendarIdGet, p, client2)
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/handlers/tenancy.ts
|
|
576
|
+
function tenancyHandlers(client2) {
|
|
577
|
+
return {
|
|
521
578
|
GET_SUBSIDIARIES: (p) => call(getAllSubsidiariesApiTenancySubsidiaryGet, p, client2),
|
|
522
579
|
GET_SUBSIDIARY: (p) => call(getSubsidiaryApiTenancySubsidiarySubsidiaryIdGet, p, client2),
|
|
523
580
|
GET_SUBSIDIARY_PARENT_CURRENCIES: (p) => call(
|
|
@@ -529,46 +586,115 @@ function createHandlerMap(options) {
|
|
|
529
586
|
};
|
|
530
587
|
}
|
|
531
588
|
|
|
532
|
-
// src/
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
589
|
+
// src/handlerMap.ts
|
|
590
|
+
function createHandlerMap(options) {
|
|
591
|
+
const client2 = createVibeApiClient(options);
|
|
592
|
+
return {
|
|
593
|
+
...accountingHandlers(client2),
|
|
594
|
+
...activityHandlers(client2),
|
|
595
|
+
...auditTrailHandlers(client2),
|
|
596
|
+
...periodManagerHandlers(client2),
|
|
597
|
+
...tenancyHandlers(client2)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/rateLimiter.ts
|
|
602
|
+
var DEFAULT_RATE_LIMIT = 60;
|
|
603
|
+
function createRateLimiter(limit = DEFAULT_RATE_LIMIT, windowMs = 6e4) {
|
|
604
|
+
return { timestamps: [], limit, windowMs };
|
|
605
|
+
}
|
|
606
|
+
function checkRateLimit(state) {
|
|
607
|
+
const now = Date.now();
|
|
608
|
+
const cutoff = now - state.windowMs;
|
|
609
|
+
state.timestamps = state.timestamps.filter((t) => t > cutoff);
|
|
610
|
+
if (state.timestamps.length >= state.limit) {
|
|
611
|
+
return false;
|
|
538
612
|
}
|
|
539
|
-
|
|
613
|
+
state.timestamps.push(now);
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/unwired.ts
|
|
618
|
+
function warnUnwired(command) {
|
|
619
|
+
return () => {
|
|
620
|
+
console.warn(`[vibe-host] Received "${command}" command but no handler is wired \u2014 ignoring.`);
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function rejectUnwired(op) {
|
|
624
|
+
return () => Promise.reject(
|
|
625
|
+
new BridgeError("NOT_WIRED", `[vibe-host] Received "${op}" request but no handler is wired.`)
|
|
626
|
+
);
|
|
540
627
|
}
|
|
541
628
|
|
|
542
629
|
// src/VibeAppHost.ts
|
|
543
630
|
var VibeAppHost = class {
|
|
544
|
-
constructor(
|
|
631
|
+
constructor(opts) {
|
|
545
632
|
this.iframeWindow = null;
|
|
633
|
+
/**
|
|
634
|
+
* Source windows we've already logged a successful handshake for. The bridge
|
|
635
|
+
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
636
|
+
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
637
|
+
*/
|
|
638
|
+
this.handshakeLoggedWindows = /* @__PURE__ */ new WeakSet();
|
|
546
639
|
this.kindHandlers = {
|
|
547
640
|
request: (msg, source, origin) => this.handleRequest(msg, source, origin),
|
|
548
641
|
command: (msg) => this.dispatchCommand(msg)
|
|
549
642
|
};
|
|
550
|
-
|
|
551
|
-
REPORT_SUBROUTE: (payload) => {
|
|
552
|
-
const newPath = payload.subroute === "/" ? this.appBasePath : `${this.appBasePath}${payload.subroute}`;
|
|
553
|
-
const method = payload.replace ? "replaceState" : "pushState";
|
|
554
|
-
window.history[method](null, "", newPath);
|
|
555
|
-
}
|
|
556
|
-
};
|
|
643
|
+
const { trustedOrigins, getContext, appBasePath, clientConfig } = opts;
|
|
557
644
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
558
645
|
this.getContext = getContext;
|
|
559
646
|
this.appBasePath = appBasePath;
|
|
647
|
+
this.onRequestComplete = opts.onRequestComplete;
|
|
648
|
+
this.rateLimiter = createRateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
|
|
560
649
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
561
650
|
this.boundHandlePopState = this.handlePopState.bind(this);
|
|
562
|
-
this.handlers = this.initializeHandlers(
|
|
651
|
+
this.handlers = this.initializeHandlers(
|
|
652
|
+
clientConfig || { baseUrl: "", stripApiPrefix: false },
|
|
653
|
+
opts
|
|
654
|
+
);
|
|
655
|
+
this.commandHandlers = this.buildCommandHandlers(opts);
|
|
563
656
|
}
|
|
564
657
|
dispatchCommand(msg) {
|
|
565
658
|
const handler = this.commandHandlers[msg.type];
|
|
566
|
-
handler
|
|
659
|
+
if (!handler) {
|
|
660
|
+
console.warn(`[vibe-host] Received unknown command "${msg.type}" \u2014 ignoring.`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
handler(msg.payload);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
console.warn(`[vibe-host] Command "${msg.type}" handler threw \u2014 ignoring.`, err);
|
|
667
|
+
}
|
|
567
668
|
}
|
|
568
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
|
|
671
|
+
* guard, item `l`); navigation/UI side effects are injected by the consumer,
|
|
672
|
+
* falling back to warn-and-ignore (DP4).
|
|
673
|
+
*/
|
|
674
|
+
buildCommandHandlers(opts) {
|
|
675
|
+
return {
|
|
676
|
+
REPORT_SUBROUTE: (payload) => {
|
|
677
|
+
const safe = normalizeSubroute(payload.subroute);
|
|
678
|
+
if (safe === null) return;
|
|
679
|
+
const newPath = safe === "/" ? this.appBasePath : `${this.appBasePath}${safe}`;
|
|
680
|
+
const method = payload.replace ? "replaceState" : "pushState";
|
|
681
|
+
window.history[method](null, "", newPath);
|
|
682
|
+
},
|
|
683
|
+
NAVIGATE: opts.onNavigate ?? warnUnwired("NAVIGATE"),
|
|
684
|
+
SET_SIDE_NAV: opts.onSetSideNav ?? warnUnwired("SET_SIDE_NAV"),
|
|
685
|
+
SWITCH_SUBSIDIARY: opts.onSwitchSubsidiary ?? warnUnwired("SWITCH_SUBSIDIARY")
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Requests: the built-in API map (hey-api), plus the injected/awaited side
|
|
690
|
+
* effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
|
|
691
|
+
* error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
|
|
692
|
+
*/
|
|
693
|
+
initializeHandlers(clientConfig, opts) {
|
|
569
694
|
return {
|
|
570
695
|
...createHandlerMap(clientConfig),
|
|
571
|
-
UPLOAD_FILE:
|
|
696
|
+
UPLOAD_FILE: opts.onUpload ?? rejectUnwired("UPLOAD_FILE"),
|
|
697
|
+
INVALIDATE_CACHE: opts.onInvalidateCache ?? (async () => ({ invalidated: false }))
|
|
572
698
|
};
|
|
573
699
|
}
|
|
574
700
|
/**
|
|
@@ -640,12 +766,33 @@ var VibeAppHost = class {
|
|
|
640
766
|
async handleRequest(msg, source, origin) {
|
|
641
767
|
const { requestId, type } = msg;
|
|
642
768
|
if (type === "GET_CONTEXT") {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
769
|
+
try {
|
|
770
|
+
const ctx = { ...this.getContext(), hostVersion: version };
|
|
771
|
+
if (!this.handshakeLoggedWindows.has(source)) {
|
|
772
|
+
this.handshakeLoggedWindows.add(source);
|
|
773
|
+
const bridgeVersion = msg.payload?.bridgeVersion ?? "unknown";
|
|
774
|
+
console.log(
|
|
775
|
+
`[vibe-host] Vibe App connected \u2014 bridge: ${bridgeVersion}, host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data: ctx });
|
|
779
|
+
} catch (err) {
|
|
780
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
781
|
+
const code = err instanceof BridgeError ? err.code : "CONTEXT_ERROR";
|
|
782
|
+
this.respond(source, origin, { kind: "response", type, requestId, ok: false, error, code });
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (!checkRateLimit(this.rateLimiter)) {
|
|
787
|
+
this.respond(source, origin, {
|
|
788
|
+
kind: "response",
|
|
789
|
+
type,
|
|
790
|
+
requestId,
|
|
791
|
+
ok: false,
|
|
792
|
+
error: `Too many requests. Limit is ${this.rateLimiter.limit} per minute.`,
|
|
793
|
+
code: "RATE_LIMITED"
|
|
794
|
+
});
|
|
795
|
+
this.onRequestComplete?.({ type, ok: false, payload: msg.payload });
|
|
649
796
|
return;
|
|
650
797
|
}
|
|
651
798
|
const sendProgress = (payload) => {
|
|
@@ -663,9 +810,21 @@ var VibeAppHost = class {
|
|
|
663
810
|
const handler = this.handlers[type];
|
|
664
811
|
const data = await handler(msg.payload, sendProgress);
|
|
665
812
|
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data });
|
|
813
|
+
this.onRequestComplete?.({ type, ok: true, payload: msg.payload });
|
|
666
814
|
} catch (err) {
|
|
667
815
|
const error = err instanceof Error ? err.message : String(err);
|
|
668
|
-
|
|
816
|
+
const code = err instanceof BridgeError ? err.code : void 0;
|
|
817
|
+
const status = err instanceof HttpBridgeError ? err.status : void 0;
|
|
818
|
+
this.respond(source, origin, {
|
|
819
|
+
kind: "response",
|
|
820
|
+
type,
|
|
821
|
+
requestId,
|
|
822
|
+
ok: false,
|
|
823
|
+
error,
|
|
824
|
+
code,
|
|
825
|
+
status
|
|
826
|
+
});
|
|
827
|
+
this.onRequestComplete?.({ type, ok: false, payload: msg.payload });
|
|
669
828
|
}
|
|
670
829
|
}
|
|
671
830
|
respond(target, origin, response) {
|
|
@@ -677,5 +836,7 @@ var VibeAppHost = class {
|
|
|
677
836
|
};
|
|
678
837
|
// Annotate the CommonJS export names for ESM import in node:
|
|
679
838
|
0 && (module.exports = {
|
|
839
|
+
BridgeError,
|
|
840
|
+
HttpBridgeError,
|
|
680
841
|
VibeAppHost
|
|
681
842
|
});
|