@nominalso/vibe-host 0.1.0 → 0.3.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 +21 -7
- package/dist/index.cjs +261 -55
- package/dist/index.d.cts +191 -16
- package/dist/index.d.ts +191 -16
- package/dist/index.js +259 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ Externalizes only `@hey-api/client-fetch`; TypeScript types are self-contained.
|
|
|
18
18
|
import { VibeAppHost } from '@nominalso/vibe-host'
|
|
19
19
|
|
|
20
20
|
const host = new VibeAppHost({
|
|
21
|
-
// Iframe origin(s) allowed to talk to this host
|
|
21
|
+
// Iframe origin(s) allowed to talk to this host. Exact origins, or glob
|
|
22
|
+
// patterns (e.g. 'https://*.vercel.app') — add patterns only in non-prod.
|
|
22
23
|
trustedOrigins: ['https://my-vibe-app.lovable.app'],
|
|
23
24
|
// This Vibe App's base path in nom-ui (used to sync the browser URL).
|
|
24
25
|
appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
@@ -37,15 +38,28 @@ const unmount = host.mount()
|
|
|
37
38
|
|
|
38
39
|
### `new VibeAppHost(options)`
|
|
39
40
|
|
|
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 }`.
|
|
41
|
+
| Option | Type | Description |
|
|
42
|
+
| ---------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `trustedOrigins` | `string[]` | Iframe origins allowed to talk to this host — exact origins or glob patterns (`https://*.vercel.app`, honoured unconditionally, so add only in non-prod). Messages from other origins are ignored. |
|
|
44
|
+
| `getContext` | `() => HostContext` | Returns the current context to send to the iframe (`hostVersion` is injected automatically). |
|
|
45
|
+
| `appBasePath` | `string` | Base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`. |
|
|
46
|
+
| `clientConfig` | `VibeApiClientOptions?` | Points the built-in handler map at the Nominal API. Defaults to `{ baseUrl: '', stripApiPrefix: false }`. |
|
|
46
47
|
|
|
47
48
|
`VibeApiClientOptions` is `{ baseUrl: string; stripApiPrefix?: boolean }`. Set `stripApiPrefix: true` when routing through a proxy whose convention expects paths without the `/api` prefix.
|
|
48
49
|
|
|
50
|
+
#### Testing preview deployments with `trustedOrigins` patterns
|
|
51
|
+
|
|
52
|
+
A pattern entry's `*` matches exactly one DNS label or port — anchored, scheme literal — so `https://*.vercel.app` matches `https://pr-7.vercel.app` but not `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`. Patterns are honoured unconditionally, so **gate the list on your own deploy environment** — keep production exact-only:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const trustedOrigins =
|
|
56
|
+
process.env.NOM_ENV === 'production'
|
|
57
|
+
? ['https://app.nominal.so']
|
|
58
|
+
: ['https://app.nominal.so', 'https://*.vercel.app', 'https://*.lovable.app']
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Because you cannot `postMessage` to a pattern, proactive context/subroute pushes target the exact entries plus any origin seen on a validated inbound message; a pattern-matched iframe still receives context via its `connect()` poll response.
|
|
62
|
+
|
|
49
63
|
### `mount(iframeWindow?): () => void`
|
|
50
64
|
|
|
51
65
|
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`.
|
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.
|
|
30
|
+
var version = "0.3.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,47 @@ 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
|
+
};
|
|
75
|
+
function isOriginPattern(entry) {
|
|
76
|
+
return entry.includes("*");
|
|
77
|
+
}
|
|
78
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
79
|
+
function patternToRegExp(pattern) {
|
|
80
|
+
const cached = patternCache.get(pattern);
|
|
81
|
+
if (cached) return cached;
|
|
82
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
const body = escaped.replace(/\\\*/g, "[^./:@?#\\s]+");
|
|
84
|
+
const compiled = new RegExp(`^${body}$`);
|
|
85
|
+
patternCache.set(pattern, compiled);
|
|
86
|
+
return compiled;
|
|
87
|
+
}
|
|
88
|
+
function matchesOrigin(origin, entry) {
|
|
89
|
+
if (!isOriginPattern(entry)) return origin === entry;
|
|
90
|
+
return patternToRegExp(entry).test(origin);
|
|
91
|
+
}
|
|
92
|
+
function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
93
|
+
for (const entry of allowlist) {
|
|
94
|
+
if (isOriginPattern(entry)) {
|
|
95
|
+
if (allowPatterns && matchesOrigin(origin, entry)) return true;
|
|
96
|
+
} else if (origin === entry) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
47
102
|
|
|
48
103
|
// ../api-client/dist/index.js
|
|
49
104
|
var import_client_fetch = require("@hey-api/client-fetch");
|
|
@@ -375,10 +430,6 @@ var getSubsidiaryParentCurrenciesApiTenancySubsidiarySubsidiaryIdParentCurrencie
|
|
|
375
430
|
};
|
|
376
431
|
function createVibeApiClient({ baseUrl, stripApiPrefix }) {
|
|
377
432
|
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
433
|
if (stripApiPrefix) {
|
|
383
434
|
client2.interceptors.request.use(
|
|
384
435
|
(request) => new Request(request.url.replace(`${baseUrl}/api/`, `${baseUrl}/`), request)
|
|
@@ -387,16 +438,28 @@ function createVibeApiClient({ baseUrl, stripApiPrefix }) {
|
|
|
387
438
|
return client2;
|
|
388
439
|
}
|
|
389
440
|
|
|
390
|
-
// src/
|
|
441
|
+
// src/handlers/call.ts
|
|
391
442
|
async function call(fn, payload, client2) {
|
|
392
|
-
const { data } = await fn({ ...payload, client: client2 });
|
|
393
|
-
if (data == null)
|
|
443
|
+
const { data, response } = await fn({ ...payload, client: client2 });
|
|
444
|
+
if (data == null) {
|
|
445
|
+
if (response && !response.ok) {
|
|
446
|
+
throw new HttpBridgeError(
|
|
447
|
+
response.status,
|
|
448
|
+
`API request failed with status ${response.status}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (!response) {
|
|
452
|
+
throw new Error("API request produced no response");
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
394
456
|
return data;
|
|
395
457
|
}
|
|
396
|
-
|
|
397
|
-
|
|
458
|
+
|
|
459
|
+
// src/handlers/accounting.ts
|
|
460
|
+
function accountingHandlers(client2) {
|
|
398
461
|
return {
|
|
399
|
-
//
|
|
462
|
+
// Chart of accounts
|
|
400
463
|
GET_CHART_OF_ACCOUNTS: (p) => call(getCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGet, p, client2),
|
|
401
464
|
GET_COA_TREE: (p) => call(getCoaTreeApiAccountingSubsidiarySubsidiaryIdCoaGet, p, client2),
|
|
402
465
|
GET_COA_FLAT_SIMPLE: (p) => call(
|
|
@@ -421,7 +484,7 @@ function createHandlerMap(options) {
|
|
|
421
484
|
),
|
|
422
485
|
GET_ACCOUNTS: (p) => call(getAccountsApiAccountingAccountGet, p, client2),
|
|
423
486
|
GET_ACCOUNT: (p) => call(getAccountApiAccountingAccountAccountIdGet, p, client2),
|
|
424
|
-
//
|
|
487
|
+
// Exchange rates
|
|
425
488
|
GET_CONVERSION_RATES: (p) => call(getConversionRateApiAccountingCurrencyCurrencyConversionRatesGet, p, client2),
|
|
426
489
|
GET_EFFECTIVE_EXCHANGE_RATE: (p) => call(getEffectiveExchangeRateApiAccountingConsolidationExchangeRateGet, p, client2),
|
|
427
490
|
GET_EXCHANGE_RATE_BY_DATE: (p) => call(
|
|
@@ -429,7 +492,7 @@ function createHandlerMap(options) {
|
|
|
429
492
|
p,
|
|
430
493
|
client2
|
|
431
494
|
),
|
|
432
|
-
//
|
|
495
|
+
// Dimensions
|
|
433
496
|
GET_DIMENSIONS: (p) => call(getDimensionsApiAccountingDimensionGet, p, client2),
|
|
434
497
|
GET_DIMENSION_VALUES: (p) => call(getDimensionValuesApiAccountingDimensionValueGet, p, client2),
|
|
435
498
|
GET_DIMENSION_VALUES_HIERARCHICAL: (p) => call(
|
|
@@ -442,7 +505,7 @@ function createHandlerMap(options) {
|
|
|
442
505
|
p,
|
|
443
506
|
client2
|
|
444
507
|
),
|
|
445
|
-
//
|
|
508
|
+
// Journal entries
|
|
446
509
|
GET_JOURNAL_ENTRIES: (p) => call(
|
|
447
510
|
getSubsidiaryJournalApiAccountingSubsidiarySubsidiaryIdJournalEntryGet,
|
|
448
511
|
p,
|
|
@@ -457,8 +520,14 @@ function createHandlerMap(options) {
|
|
|
457
520
|
getJournalEntryApiAccountingSubsidiarySubsidiaryIdJournalEntryJournalEntryIdGet,
|
|
458
521
|
p,
|
|
459
522
|
client2
|
|
460
|
-
)
|
|
461
|
-
|
|
523
|
+
)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/handlers/activity.ts
|
|
528
|
+
function activityHandlers(client2) {
|
|
529
|
+
return {
|
|
530
|
+
// Period instances
|
|
462
531
|
GET_PERIODS: (p) => call(getPeriodInstancesApiActivityPeriodInstanceGet, p, client2),
|
|
463
532
|
GET_PERIOD_INSTANCE: (p) => call(getPeriodInstanceApiActivityPeriodInstancePeriodInstanceIdGet, p, client2),
|
|
464
533
|
GET_PERIOD_INSTANCE_BY_SLUG: (p) => call(
|
|
@@ -471,14 +540,14 @@ function createHandlerMap(options) {
|
|
|
471
540
|
p,
|
|
472
541
|
client2
|
|
473
542
|
),
|
|
474
|
-
// Activity
|
|
543
|
+
// Activity definitions
|
|
475
544
|
GET_ACTIVITY_DEFINITIONS: (p) => call(getActivityDefinitionsApiActivityActivityDefinitionGet, p, client2),
|
|
476
545
|
GET_ACTIVITY_DEFINITION: (p) => call(
|
|
477
546
|
getActivityDefinitionByIdApiActivityActivityDefinitionActivityDefinitionIdGet,
|
|
478
547
|
p,
|
|
479
548
|
client2
|
|
480
549
|
),
|
|
481
|
-
// Activity
|
|
550
|
+
// Activity instances
|
|
482
551
|
GET_ACTIVITY_INSTANCES: (p) => call(getActivityInstancesApiActivityActivityInstanceGet, p, client2),
|
|
483
552
|
GET_ACTIVITY_INSTANCE: (p) => call(getActivityInstanceApiActivityActivityInstanceActivityInstanceIdGet, p, client2),
|
|
484
553
|
GET_ACTIVITY_INSTANCE_BY_PERIOD: (p) => call(
|
|
@@ -496,28 +565,43 @@ function createHandlerMap(options) {
|
|
|
496
565
|
p,
|
|
497
566
|
client2
|
|
498
567
|
),
|
|
499
|
-
//
|
|
568
|
+
// Task definitions
|
|
500
569
|
GET_TASK_DEFINITIONS: (p) => call(getAllTasksDefinitionsApiActivityTaskDefinitionGet, p, client2),
|
|
501
570
|
GET_TASK_DEFINITION: (p) => call(getTaskDefinitionByIdApiActivityTaskDefinitionTaskDefinitionIdGet, p, client2),
|
|
502
571
|
CREATE_TASK_DEFINITION: (p) => call(createTaskDefinitionApiActivityTaskDefinitionPost, p, client2),
|
|
503
572
|
UPDATE_TASK_DEFINITION: (p) => call(updateTaskDefinitionApiActivityTaskDefinitionTaskDefinitionIdPut, p, client2),
|
|
504
573
|
GET_TASK_DEFINITIONS_BY_FILTER: (p) => call(getAllTasksDefinitionsByFilterApiActivityTaskDefinitionQueryPost, p, client2),
|
|
505
|
-
//
|
|
574
|
+
// Task instances
|
|
506
575
|
GET_TASK_INSTANCES: (p) => call(getAllTaskInstancesApiActivityTaskInstanceGet, p, client2),
|
|
507
576
|
GET_TASK_INSTANCE: (p) => call(getTaskInstanceByIdApiActivityTaskInstanceTaskInstanceIdGet, p, client2),
|
|
508
577
|
POST_TASK_OUTPUT: (p) => call(updateTaskInstanceApiActivityTaskInstanceTaskInstanceIdPut, p, client2),
|
|
509
|
-
GET_TASK_INSTANCES_BY_FILTER: (p) => call(getAllTaskInstancesByFilterApiActivityTaskInstanceQueryPost, p, client2)
|
|
510
|
-
|
|
578
|
+
GET_TASK_INSTANCES_BY_FILTER: (p) => call(getAllTaskInstancesByFilterApiActivityTaskInstanceQueryPost, p, client2)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/handlers/audit-trail.ts
|
|
583
|
+
function auditTrailHandlers(client2) {
|
|
584
|
+
return {
|
|
511
585
|
GET_AUDIT_EVENTS: (p) => call(getEventsApiAuditTrailEventGet, p, client2),
|
|
512
|
-
GET_ENTITY_AUDIT_EVENTS: (p) => call(getEntityEventsApiAuditTrailEventEntityTargetTypeTargetIdPost, p, client2)
|
|
513
|
-
|
|
586
|
+
GET_ENTITY_AUDIT_EVENTS: (p) => call(getEntityEventsApiAuditTrailEventEntityTargetTypeTargetIdPost, p, client2)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/handlers/period-manager.ts
|
|
591
|
+
function periodManagerHandlers(client2) {
|
|
592
|
+
return {
|
|
514
593
|
GET_FISCAL_CALENDARS: (p) => call(
|
|
515
594
|
getFiscalCalendarsBySubsidiaryApiPeriodManagerFiscalCalendarBySubsidiarySubsidiaryIdGet,
|
|
516
595
|
p,
|
|
517
596
|
client2
|
|
518
597
|
),
|
|
519
|
-
GET_FISCAL_CALENDAR: (p) => call(getFiscalCalendarByIdApiPeriodManagerFiscalCalendarCalendarIdGet, p, client2)
|
|
520
|
-
|
|
598
|
+
GET_FISCAL_CALENDAR: (p) => call(getFiscalCalendarByIdApiPeriodManagerFiscalCalendarCalendarIdGet, p, client2)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/handlers/tenancy.ts
|
|
603
|
+
function tenancyHandlers(client2) {
|
|
604
|
+
return {
|
|
521
605
|
GET_SUBSIDIARIES: (p) => call(getAllSubsidiariesApiTenancySubsidiaryGet, p, client2),
|
|
522
606
|
GET_SUBSIDIARY: (p) => call(getSubsidiaryApiTenancySubsidiarySubsidiaryIdGet, p, client2),
|
|
523
607
|
GET_SUBSIDIARY_PARENT_CURRENCIES: (p) => call(
|
|
@@ -529,46 +613,122 @@ function createHandlerMap(options) {
|
|
|
529
613
|
};
|
|
530
614
|
}
|
|
531
615
|
|
|
532
|
-
// src/
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
616
|
+
// src/handlerMap.ts
|
|
617
|
+
function createHandlerMap(options) {
|
|
618
|
+
const client2 = createVibeApiClient(options);
|
|
619
|
+
return {
|
|
620
|
+
...accountingHandlers(client2),
|
|
621
|
+
...activityHandlers(client2),
|
|
622
|
+
...auditTrailHandlers(client2),
|
|
623
|
+
...periodManagerHandlers(client2),
|
|
624
|
+
...tenancyHandlers(client2)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/rateLimiter.ts
|
|
629
|
+
var DEFAULT_RATE_LIMIT = 60;
|
|
630
|
+
function createRateLimiter(limit = DEFAULT_RATE_LIMIT, windowMs = 6e4) {
|
|
631
|
+
return { timestamps: [], limit, windowMs };
|
|
632
|
+
}
|
|
633
|
+
function checkRateLimit(state) {
|
|
634
|
+
const now = Date.now();
|
|
635
|
+
const cutoff = now - state.windowMs;
|
|
636
|
+
state.timestamps = state.timestamps.filter((t) => t > cutoff);
|
|
637
|
+
if (state.timestamps.length >= state.limit) {
|
|
638
|
+
return false;
|
|
538
639
|
}
|
|
539
|
-
|
|
640
|
+
state.timestamps.push(now);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/unwired.ts
|
|
645
|
+
function warnUnwired(command) {
|
|
646
|
+
return () => {
|
|
647
|
+
console.warn(`[vibe-host] Received "${command}" command but no handler is wired \u2014 ignoring.`);
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function rejectUnwired(op) {
|
|
651
|
+
return () => Promise.reject(
|
|
652
|
+
new BridgeError("NOT_WIRED", `[vibe-host] Received "${op}" request but no handler is wired.`)
|
|
653
|
+
);
|
|
540
654
|
}
|
|
541
655
|
|
|
542
656
|
// src/VibeAppHost.ts
|
|
543
657
|
var VibeAppHost = class {
|
|
544
|
-
constructor(
|
|
658
|
+
constructor(opts) {
|
|
659
|
+
/**
|
|
660
|
+
* Concrete iframe origins observed on validated inbound messages. Because you
|
|
661
|
+
* cannot `postMessage` to a glob pattern, proactive pushes target these
|
|
662
|
+
* learned origins (plus the exact allowlist entries) rather than the patterns.
|
|
663
|
+
*/
|
|
664
|
+
this.connectedOrigins = /* @__PURE__ */ new Set();
|
|
545
665
|
this.iframeWindow = null;
|
|
666
|
+
/**
|
|
667
|
+
* Source windows we've already logged a successful handshake for. The bridge
|
|
668
|
+
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
669
|
+
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
670
|
+
*/
|
|
671
|
+
this.handshakeLoggedWindows = /* @__PURE__ */ new WeakSet();
|
|
546
672
|
this.kindHandlers = {
|
|
547
673
|
request: (msg, source, origin) => this.handleRequest(msg, source, origin),
|
|
548
674
|
command: (msg) => this.dispatchCommand(msg)
|
|
549
675
|
};
|
|
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
|
-
};
|
|
676
|
+
const { trustedOrigins, getContext, appBasePath, clientConfig } = opts;
|
|
557
677
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
678
|
+
this.exactOrigins = trustedOrigins.filter((entry) => !isOriginPattern(entry));
|
|
558
679
|
this.getContext = getContext;
|
|
559
680
|
this.appBasePath = appBasePath;
|
|
681
|
+
this.onRequestComplete = opts.onRequestComplete;
|
|
682
|
+
this.rateLimiter = createRateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
|
|
560
683
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
561
684
|
this.boundHandlePopState = this.handlePopState.bind(this);
|
|
562
|
-
this.handlers = this.initializeHandlers(
|
|
685
|
+
this.handlers = this.initializeHandlers(
|
|
686
|
+
clientConfig || { baseUrl: "", stripApiPrefix: false },
|
|
687
|
+
opts
|
|
688
|
+
);
|
|
689
|
+
this.commandHandlers = this.buildCommandHandlers(opts);
|
|
563
690
|
}
|
|
564
691
|
dispatchCommand(msg) {
|
|
565
692
|
const handler = this.commandHandlers[msg.type];
|
|
566
|
-
handler
|
|
693
|
+
if (!handler) {
|
|
694
|
+
console.warn(`[vibe-host] Received unknown command "${msg.type}" \u2014 ignoring.`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
handler(msg.payload);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.warn(`[vibe-host] Command "${msg.type}" handler threw \u2014 ignoring.`, err);
|
|
701
|
+
}
|
|
567
702
|
}
|
|
568
|
-
|
|
703
|
+
/**
|
|
704
|
+
* Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
|
|
705
|
+
* guard, item `l`); navigation/UI side effects are injected by the consumer,
|
|
706
|
+
* falling back to warn-and-ignore (DP4).
|
|
707
|
+
*/
|
|
708
|
+
buildCommandHandlers(opts) {
|
|
709
|
+
return {
|
|
710
|
+
REPORT_SUBROUTE: (payload) => {
|
|
711
|
+
const safe = normalizeSubroute(payload.subroute);
|
|
712
|
+
if (safe === null) return;
|
|
713
|
+
const newPath = safe === "/" ? this.appBasePath : `${this.appBasePath}${safe}`;
|
|
714
|
+
const method = payload.replace ? "replaceState" : "pushState";
|
|
715
|
+
window.history[method](null, "", newPath);
|
|
716
|
+
},
|
|
717
|
+
NAVIGATE: opts.onNavigate ?? warnUnwired("NAVIGATE"),
|
|
718
|
+
SET_SIDE_NAV: opts.onSetSideNav ?? warnUnwired("SET_SIDE_NAV"),
|
|
719
|
+
SWITCH_SUBSIDIARY: opts.onSwitchSubsidiary ?? warnUnwired("SWITCH_SUBSIDIARY")
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Requests: the built-in API map (hey-api), plus the injected/awaited side
|
|
724
|
+
* effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
|
|
725
|
+
* error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
|
|
726
|
+
*/
|
|
727
|
+
initializeHandlers(clientConfig, opts) {
|
|
569
728
|
return {
|
|
570
729
|
...createHandlerMap(clientConfig),
|
|
571
|
-
UPLOAD_FILE:
|
|
730
|
+
UPLOAD_FILE: opts.onUpload ?? rejectUnwired("UPLOAD_FILE"),
|
|
731
|
+
INVALIDATE_CACHE: opts.onInvalidateCache ?? (async () => ({ invalidated: false }))
|
|
572
732
|
};
|
|
573
733
|
}
|
|
574
734
|
/**
|
|
@@ -589,6 +749,7 @@ var VibeAppHost = class {
|
|
|
589
749
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
590
750
|
window.removeEventListener("popstate", this.boundHandlePopState);
|
|
591
751
|
this.iframeWindow = null;
|
|
752
|
+
this.connectedOrigins.clear();
|
|
592
753
|
};
|
|
593
754
|
}
|
|
594
755
|
/**
|
|
@@ -608,7 +769,7 @@ var VibeAppHost = class {
|
|
|
608
769
|
type: "CONTEXT_PUSH",
|
|
609
770
|
payload: { ...this.getContext(), hostVersion: version }
|
|
610
771
|
};
|
|
611
|
-
for (const origin of this.
|
|
772
|
+
for (const origin of this.concreteTargets()) {
|
|
612
773
|
target.postMessage(message, origin);
|
|
613
774
|
}
|
|
614
775
|
}
|
|
@@ -628,24 +789,55 @@ var VibeAppHost = class {
|
|
|
628
789
|
type,
|
|
629
790
|
payload
|
|
630
791
|
};
|
|
631
|
-
for (const origin of this.
|
|
792
|
+
for (const origin of this.concreteTargets()) {
|
|
632
793
|
this.iframeWindow.postMessage(message, origin);
|
|
633
794
|
}
|
|
634
795
|
}
|
|
635
796
|
async handleMessage(event) {
|
|
636
|
-
if (!
|
|
797
|
+
if (!isOriginAllowed(event.origin, this.trustedOrigins, { allowPatterns: true })) return;
|
|
637
798
|
if (!isBridgeMessage(event.data)) return;
|
|
799
|
+
this.connectedOrigins.add(event.origin);
|
|
638
800
|
await this.kindHandlers[event.data.kind]?.(event.data, event.source, event.origin);
|
|
639
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
804
|
+
* plus any origins seen on validated inbound messages. Never includes glob
|
|
805
|
+
* patterns (you cannot `postMessage` to one). When only exact origins are
|
|
806
|
+
* configured this equals the configured set, so behaviour is unchanged.
|
|
807
|
+
*/
|
|
808
|
+
concreteTargets() {
|
|
809
|
+
return /* @__PURE__ */ new Set([...this.exactOrigins, ...this.connectedOrigins]);
|
|
810
|
+
}
|
|
640
811
|
async handleRequest(msg, source, origin) {
|
|
641
812
|
const { requestId, type } = msg;
|
|
642
813
|
if (type === "GET_CONTEXT") {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
814
|
+
try {
|
|
815
|
+
const ctx = { ...this.getContext(), hostVersion: version };
|
|
816
|
+
if (!this.handshakeLoggedWindows.has(source)) {
|
|
817
|
+
this.handshakeLoggedWindows.add(source);
|
|
818
|
+
const bridgeVersion = msg.payload?.bridgeVersion ?? "unknown";
|
|
819
|
+
console.log(
|
|
820
|
+
`[vibe-host] Vibe App connected \u2014 bridge: ${bridgeVersion}, host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data: ctx });
|
|
824
|
+
} catch (err) {
|
|
825
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
826
|
+
const code = err instanceof BridgeError ? err.code : "CONTEXT_ERROR";
|
|
827
|
+
this.respond(source, origin, { kind: "response", type, requestId, ok: false, error, code });
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (!checkRateLimit(this.rateLimiter)) {
|
|
832
|
+
this.respond(source, origin, {
|
|
833
|
+
kind: "response",
|
|
834
|
+
type,
|
|
835
|
+
requestId,
|
|
836
|
+
ok: false,
|
|
837
|
+
error: `Too many requests. Limit is ${this.rateLimiter.limit} per minute.`,
|
|
838
|
+
code: "RATE_LIMITED"
|
|
839
|
+
});
|
|
840
|
+
this.onRequestComplete?.({ type, ok: false, payload: msg.payload });
|
|
649
841
|
return;
|
|
650
842
|
}
|
|
651
843
|
const sendProgress = (payload) => {
|
|
@@ -663,9 +855,21 @@ var VibeAppHost = class {
|
|
|
663
855
|
const handler = this.handlers[type];
|
|
664
856
|
const data = await handler(msg.payload, sendProgress);
|
|
665
857
|
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data });
|
|
858
|
+
this.onRequestComplete?.({ type, ok: true, payload: msg.payload });
|
|
666
859
|
} catch (err) {
|
|
667
860
|
const error = err instanceof Error ? err.message : String(err);
|
|
668
|
-
|
|
861
|
+
const code = err instanceof BridgeError ? err.code : void 0;
|
|
862
|
+
const status = err instanceof HttpBridgeError ? err.status : void 0;
|
|
863
|
+
this.respond(source, origin, {
|
|
864
|
+
kind: "response",
|
|
865
|
+
type,
|
|
866
|
+
requestId,
|
|
867
|
+
ok: false,
|
|
868
|
+
error,
|
|
869
|
+
code,
|
|
870
|
+
status
|
|
871
|
+
});
|
|
872
|
+
this.onRequestComplete?.({ type, ok: false, payload: msg.payload });
|
|
669
873
|
}
|
|
670
874
|
}
|
|
671
875
|
respond(target, origin, response) {
|
|
@@ -677,5 +881,7 @@ var VibeAppHost = class {
|
|
|
677
881
|
};
|
|
678
882
|
// Annotate the CommonJS export names for ESM import in node:
|
|
679
883
|
0 && (module.exports = {
|
|
884
|
+
BridgeError,
|
|
885
|
+
HttpBridgeError,
|
|
680
886
|
VibeAppHost
|
|
681
887
|
});
|