@nominalso/vibe-host 0.1.0 → 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/dist/index.cjs +213 -52
- package/dist/index.d.cts +155 -14
- package/dist/index.d.ts +155 -14
- package/dist/index.js +211 -52
- package/package.json +1 -1
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.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
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -3201,15 +3201,6 @@ interface GetContextPayload {
|
|
|
3201
3201
|
bridgeVersion: string;
|
|
3202
3202
|
}
|
|
3203
3203
|
|
|
3204
|
-
/**
|
|
3205
|
-
* All operations the iframe can invoke on the host, each with a typed request
|
|
3206
|
-
* payload and a typed response. Adding a new operation here is all that's
|
|
3207
|
-
* required — message union types and handler types below are derived
|
|
3208
|
-
* automatically.
|
|
3209
|
-
*
|
|
3210
|
-
* Operations that also emit intermediate progress events should include a
|
|
3211
|
-
* `progress` field with the progress payload type.
|
|
3212
|
-
*/
|
|
3213
3204
|
interface UploadPayload {
|
|
3214
3205
|
buffer: ArrayBuffer;
|
|
3215
3206
|
fileName: string;
|
|
@@ -3220,11 +3211,57 @@ interface UploadPayload {
|
|
|
3220
3211
|
interface UploadResponse {
|
|
3221
3212
|
attachmentId: string;
|
|
3222
3213
|
name: string;
|
|
3214
|
+
/** Public/presigned URL of the stored file, when the host returns one. */
|
|
3215
|
+
url?: string;
|
|
3223
3216
|
}
|
|
3224
3217
|
interface UploadProgress {
|
|
3225
3218
|
attachmentId: string;
|
|
3226
3219
|
progress: number;
|
|
3227
3220
|
}
|
|
3221
|
+
/** How the host should apply a navigation. Defaults to `'push'`. */
|
|
3222
|
+
type NavigateAction = 'push' | 'replace' | 'refresh';
|
|
3223
|
+
/**
|
|
3224
|
+
* Tells the host to navigate. Provide **either** a raw `path` (relative to the
|
|
3225
|
+
* app's tenant/subsidiary base) **or** a named `route` resolved host-side.
|
|
3226
|
+
*/
|
|
3227
|
+
interface NavigatePayload {
|
|
3228
|
+
/** Raw path relative to the app's base, e.g. `'/journal-entries/123'`. */
|
|
3229
|
+
path?: string;
|
|
3230
|
+
/** Named route key the host resolves, e.g. `'CHART_OF_ACCOUNTS'`. */
|
|
3231
|
+
route?: string;
|
|
3232
|
+
/** Values for route placeholders, e.g. `{ id: '123' }`. */
|
|
3233
|
+
routeParams?: Record<string, string>;
|
|
3234
|
+
/** Navigation method. Defaults to `'push'`. */
|
|
3235
|
+
action?: NavigateAction;
|
|
3236
|
+
}
|
|
3237
|
+
/** Collapse/expand the host's side navigation. */
|
|
3238
|
+
interface SetSideNavPayload {
|
|
3239
|
+
collapsed: boolean;
|
|
3240
|
+
}
|
|
3241
|
+
/** Switch the host to a different subsidiary. */
|
|
3242
|
+
interface SwitchSubsidiaryPayload {
|
|
3243
|
+
subsidiaryId: number;
|
|
3244
|
+
}
|
|
3245
|
+
/** Targets for a host cache invalidation. */
|
|
3246
|
+
interface InvalidateCachePayload {
|
|
3247
|
+
/** API path prefixes to invalidate, e.g. `['/accounting/accounts']`. */
|
|
3248
|
+
paths?: string[];
|
|
3249
|
+
/** Cache tag keys to invalidate, e.g. `['REPORTS', 'SUBSIDIARIES']`. */
|
|
3250
|
+
tags?: string[];
|
|
3251
|
+
}
|
|
3252
|
+
/** Result of a host cache invalidation. */
|
|
3253
|
+
interface InvalidateResult {
|
|
3254
|
+
invalidated: boolean;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
/**
|
|
3258
|
+
* All operations the iframe can invoke on the host, each with a typed request
|
|
3259
|
+
* payload and a typed response. Adding a new operation here is all that's
|
|
3260
|
+
* required — message union types and handler types are derived automatically.
|
|
3261
|
+
*
|
|
3262
|
+
* Operations that also emit intermediate progress events should include a
|
|
3263
|
+
* `progress` field with the progress payload type.
|
|
3264
|
+
*/
|
|
3228
3265
|
interface RequestRegistry {
|
|
3229
3266
|
GET_CHART_OF_ACCOUNTS: {
|
|
3230
3267
|
payload: Omit<GetCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGetData, 'url'>;
|
|
@@ -3410,6 +3447,10 @@ interface RequestRegistry {
|
|
|
3410
3447
|
payload: Omit<GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetData, 'url'>;
|
|
3411
3448
|
data: GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetResponse;
|
|
3412
3449
|
};
|
|
3450
|
+
INVALIDATE_CACHE: {
|
|
3451
|
+
payload: InvalidateCachePayload;
|
|
3452
|
+
data: InvalidateResult;
|
|
3453
|
+
};
|
|
3413
3454
|
GET_CONTEXT: {
|
|
3414
3455
|
payload: GetContextPayload;
|
|
3415
3456
|
data: ContextPayload;
|
|
@@ -3434,6 +3475,53 @@ type RequestHandlers = {
|
|
|
3434
3475
|
} ? (payload: RequestRegistry[K]['payload'], sendProgress: (p: P) => void) => Promise<RequestRegistry[K]['data']> : (payload: RequestRegistry[K]['payload']) => Promise<RequestRegistry[K]['data']>;
|
|
3435
3476
|
};
|
|
3436
3477
|
|
|
3478
|
+
/**
|
|
3479
|
+
* An error that crossed the bridge, carrying a machine-readable {@link code}
|
|
3480
|
+
* alongside the human-readable message.
|
|
3481
|
+
*
|
|
3482
|
+
* The host attaches a `code` to failed responses (e.g. `'RATE_LIMITED'`,
|
|
3483
|
+
* `'FILE_TOO_LARGE'`, `'REQUEST_FAILED'`); the bridge rehydrates it into a
|
|
3484
|
+
* `BridgeError` so app `catch` blocks can branch on `error.code` instead of
|
|
3485
|
+
* string-matching `error.message`. A failed Nominal API call rehydrates as the
|
|
3486
|
+
* {@link HttpBridgeError} subtype, which adds the HTTP `status`.
|
|
3487
|
+
*
|
|
3488
|
+
* @example
|
|
3489
|
+
* ```ts
|
|
3490
|
+
* try {
|
|
3491
|
+
* await bridge.getAccounts({})
|
|
3492
|
+
* } catch (err) {
|
|
3493
|
+
* if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
|
|
3494
|
+
* // back off and retry
|
|
3495
|
+
* }
|
|
3496
|
+
* }
|
|
3497
|
+
* ```
|
|
3498
|
+
*/
|
|
3499
|
+
declare class BridgeError extends Error {
|
|
3500
|
+
readonly code: string;
|
|
3501
|
+
constructor(code: string, message: string);
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* A {@link BridgeError} for a failed Nominal API call. Its `code` is always
|
|
3505
|
+
* `'REQUEST_FAILED'`; the HTTP {@link status} is carried as a structured field
|
|
3506
|
+
* so callers can branch on it (e.g. `err.status === 404`) instead of parsing
|
|
3507
|
+
* the code string.
|
|
3508
|
+
*
|
|
3509
|
+
* @example
|
|
3510
|
+
* ```ts
|
|
3511
|
+
* try {
|
|
3512
|
+
* await bridge.getAccount({ path: { account_id: 'missing' } })
|
|
3513
|
+
* } catch (err) {
|
|
3514
|
+
* if (err instanceof HttpBridgeError && err.status === 404) {
|
|
3515
|
+
* // handle not-found
|
|
3516
|
+
* }
|
|
3517
|
+
* }
|
|
3518
|
+
* ```
|
|
3519
|
+
*/
|
|
3520
|
+
declare class HttpBridgeError extends BridgeError {
|
|
3521
|
+
readonly status: number;
|
|
3522
|
+
constructor(status: number, message: string);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3437
3525
|
interface VibeApiClientOptions {
|
|
3438
3526
|
baseUrl: string;
|
|
3439
3527
|
/**
|
|
@@ -3446,6 +3534,18 @@ interface VibeApiClientOptions {
|
|
|
3446
3534
|
|
|
3447
3535
|
type HostHandlers = Omit<RequestHandlers, 'GET_CONTEXT'>;
|
|
3448
3536
|
type HostContext = Omit<ContextPayload, 'hostVersion'>;
|
|
3537
|
+
/**
|
|
3538
|
+
* Notified after each request resolves — except the `GET_CONTEXT` connect poll,
|
|
3539
|
+
* which is exempt. Used by nom-ui for SWR busting (SSOT §5j).
|
|
3540
|
+
*/
|
|
3541
|
+
interface RequestCompleteInfo {
|
|
3542
|
+
/** The operation type, e.g. `'POST_TASK_OUTPUT'`. */
|
|
3543
|
+
type: string;
|
|
3544
|
+
/** Whether the operation succeeded. */
|
|
3545
|
+
ok: boolean;
|
|
3546
|
+
/** The request payload that was handled. */
|
|
3547
|
+
payload: unknown;
|
|
3548
|
+
}
|
|
3449
3549
|
interface VibeAppHostOptions {
|
|
3450
3550
|
/**
|
|
3451
3551
|
* Iframe origins allowed to talk to this host. Messages from any other origin
|
|
@@ -3467,13 +3567,33 @@ interface VibeAppHostOptions {
|
|
|
3467
3567
|
* `{ baseUrl: '', stripApiPrefix: false }`.
|
|
3468
3568
|
*/
|
|
3469
3569
|
clientConfig?: VibeApiClientOptions;
|
|
3570
|
+
/** Fire-and-forget. Resolve a path / named route and push to the host router. */
|
|
3571
|
+
onNavigate?: (payload: NavigatePayload) => void;
|
|
3572
|
+
/** Fire-and-forget. Collapse/expand the host side nav. */
|
|
3573
|
+
onSetSideNav?: (payload: SetSideNavPayload) => void;
|
|
3574
|
+
/** Fire-and-forget. Switch subsidiary (host owns validation + navigation). */
|
|
3575
|
+
onSwitchSubsidiary?: (payload: SwitchSubsidiaryPayload) => void;
|
|
3576
|
+
/** Awaited. Invalidate host cache by tags/paths. Defaults to a no-op. */
|
|
3577
|
+
onInvalidateCache?: (payload: InvalidateCachePayload) => Promise<InvalidateResult>;
|
|
3578
|
+
/**
|
|
3579
|
+
* Awaited. Real file upload — replaces the built-in stub (DP2). When omitted,
|
|
3580
|
+
* `UPLOAD_FILE` requests reject with a `'NOT_WIRED'` coded error.
|
|
3581
|
+
*/
|
|
3582
|
+
onUpload?: (payload: UploadPayload, sendProgress: (p: UploadProgress) => void) => Promise<UploadResponse>;
|
|
3583
|
+
/** Optional. Notified after each request resolves (except the `GET_CONTEXT` connect poll) — used by nom-ui for SWR busting (SSOT §5j). */
|
|
3584
|
+
onRequestComplete?: (info: RequestCompleteInfo) => void;
|
|
3585
|
+
/** Requests/min before a request trips `RATE_LIMITED` (DP5). Default 60. */
|
|
3586
|
+
rateLimit?: number;
|
|
3470
3587
|
}
|
|
3588
|
+
|
|
3471
3589
|
/**
|
|
3472
3590
|
* Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
|
|
3473
3591
|
* iframe origin(s), a `getContext` callback, the app's base path, and a
|
|
3474
3592
|
* `clientConfig` pointing at the Nominal API; then call {@link VibeAppHost.mount}
|
|
3475
|
-
* to start listening. The handler map for all protocol operations is built
|
|
3476
|
-
* automatically — you do not pass handlers.
|
|
3593
|
+
* to start listening. The handler map for all protocol API operations is built
|
|
3594
|
+
* automatically — you do not pass those handlers. Non-API side effects
|
|
3595
|
+
* (navigation, side nav, subsidiary switch, cache invalidation, upload) are
|
|
3596
|
+
* supplied via the optional injected callbacks.
|
|
3477
3597
|
*
|
|
3478
3598
|
* @example
|
|
3479
3599
|
* ```tsx
|
|
@@ -3483,6 +3603,8 @@ interface VibeAppHostOptions {
|
|
|
3483
3603
|
* appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
3484
3604
|
* getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
|
|
3485
3605
|
* clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
|
|
3606
|
+
* onNavigate: (p) => router.push(resolve(p)),
|
|
3607
|
+
* onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
|
|
3486
3608
|
* })
|
|
3487
3609
|
*
|
|
3488
3610
|
* useEffect(() => {
|
|
@@ -3501,16 +3623,35 @@ interface VibeAppHostOptions {
|
|
|
3501
3623
|
declare class VibeAppHost {
|
|
3502
3624
|
private readonly trustedOrigins;
|
|
3503
3625
|
private readonly handlers;
|
|
3626
|
+
private readonly commandHandlers;
|
|
3504
3627
|
private readonly getContext;
|
|
3505
3628
|
private readonly appBasePath;
|
|
3629
|
+
private readonly onRequestComplete?;
|
|
3630
|
+
private readonly rateLimiter;
|
|
3506
3631
|
private readonly boundHandleMessage;
|
|
3507
3632
|
private readonly boundHandlePopState;
|
|
3508
3633
|
private iframeWindow;
|
|
3634
|
+
/**
|
|
3635
|
+
* Source windows we've already logged a successful handshake for. The bridge
|
|
3636
|
+
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
3637
|
+
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
3638
|
+
*/
|
|
3639
|
+
private readonly handshakeLoggedWindows;
|
|
3509
3640
|
private readonly kindHandlers;
|
|
3510
3641
|
private dispatchCommand;
|
|
3511
|
-
|
|
3642
|
+
/**
|
|
3643
|
+
* Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
|
|
3644
|
+
* guard, item `l`); navigation/UI side effects are injected by the consumer,
|
|
3645
|
+
* falling back to warn-and-ignore (DP4).
|
|
3646
|
+
*/
|
|
3647
|
+
private buildCommandHandlers;
|
|
3648
|
+
/**
|
|
3649
|
+
* Requests: the built-in API map (hey-api), plus the injected/awaited side
|
|
3650
|
+
* effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
|
|
3651
|
+
* error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
|
|
3652
|
+
*/
|
|
3512
3653
|
private initializeHandlers;
|
|
3513
|
-
constructor(
|
|
3654
|
+
constructor(opts: VibeAppHostOptions);
|
|
3514
3655
|
/**
|
|
3515
3656
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
3516
3657
|
* Returns a cleanup function to call on unmount.
|
|
@@ -3535,4 +3676,4 @@ declare class VibeAppHost {
|
|
|
3535
3676
|
private respond;
|
|
3536
3677
|
}
|
|
3537
3678
|
|
|
3538
|
-
export { type ContextPayload, type HostContext, type HostHandlers, type RequestHandlers, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
|
3679
|
+
export { BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -3201,15 +3201,6 @@ interface GetContextPayload {
|
|
|
3201
3201
|
bridgeVersion: string;
|
|
3202
3202
|
}
|
|
3203
3203
|
|
|
3204
|
-
/**
|
|
3205
|
-
* All operations the iframe can invoke on the host, each with a typed request
|
|
3206
|
-
* payload and a typed response. Adding a new operation here is all that's
|
|
3207
|
-
* required — message union types and handler types below are derived
|
|
3208
|
-
* automatically.
|
|
3209
|
-
*
|
|
3210
|
-
* Operations that also emit intermediate progress events should include a
|
|
3211
|
-
* `progress` field with the progress payload type.
|
|
3212
|
-
*/
|
|
3213
3204
|
interface UploadPayload {
|
|
3214
3205
|
buffer: ArrayBuffer;
|
|
3215
3206
|
fileName: string;
|
|
@@ -3220,11 +3211,57 @@ interface UploadPayload {
|
|
|
3220
3211
|
interface UploadResponse {
|
|
3221
3212
|
attachmentId: string;
|
|
3222
3213
|
name: string;
|
|
3214
|
+
/** Public/presigned URL of the stored file, when the host returns one. */
|
|
3215
|
+
url?: string;
|
|
3223
3216
|
}
|
|
3224
3217
|
interface UploadProgress {
|
|
3225
3218
|
attachmentId: string;
|
|
3226
3219
|
progress: number;
|
|
3227
3220
|
}
|
|
3221
|
+
/** How the host should apply a navigation. Defaults to `'push'`. */
|
|
3222
|
+
type NavigateAction = 'push' | 'replace' | 'refresh';
|
|
3223
|
+
/**
|
|
3224
|
+
* Tells the host to navigate. Provide **either** a raw `path` (relative to the
|
|
3225
|
+
* app's tenant/subsidiary base) **or** a named `route` resolved host-side.
|
|
3226
|
+
*/
|
|
3227
|
+
interface NavigatePayload {
|
|
3228
|
+
/** Raw path relative to the app's base, e.g. `'/journal-entries/123'`. */
|
|
3229
|
+
path?: string;
|
|
3230
|
+
/** Named route key the host resolves, e.g. `'CHART_OF_ACCOUNTS'`. */
|
|
3231
|
+
route?: string;
|
|
3232
|
+
/** Values for route placeholders, e.g. `{ id: '123' }`. */
|
|
3233
|
+
routeParams?: Record<string, string>;
|
|
3234
|
+
/** Navigation method. Defaults to `'push'`. */
|
|
3235
|
+
action?: NavigateAction;
|
|
3236
|
+
}
|
|
3237
|
+
/** Collapse/expand the host's side navigation. */
|
|
3238
|
+
interface SetSideNavPayload {
|
|
3239
|
+
collapsed: boolean;
|
|
3240
|
+
}
|
|
3241
|
+
/** Switch the host to a different subsidiary. */
|
|
3242
|
+
interface SwitchSubsidiaryPayload {
|
|
3243
|
+
subsidiaryId: number;
|
|
3244
|
+
}
|
|
3245
|
+
/** Targets for a host cache invalidation. */
|
|
3246
|
+
interface InvalidateCachePayload {
|
|
3247
|
+
/** API path prefixes to invalidate, e.g. `['/accounting/accounts']`. */
|
|
3248
|
+
paths?: string[];
|
|
3249
|
+
/** Cache tag keys to invalidate, e.g. `['REPORTS', 'SUBSIDIARIES']`. */
|
|
3250
|
+
tags?: string[];
|
|
3251
|
+
}
|
|
3252
|
+
/** Result of a host cache invalidation. */
|
|
3253
|
+
interface InvalidateResult {
|
|
3254
|
+
invalidated: boolean;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
/**
|
|
3258
|
+
* All operations the iframe can invoke on the host, each with a typed request
|
|
3259
|
+
* payload and a typed response. Adding a new operation here is all that's
|
|
3260
|
+
* required — message union types and handler types are derived automatically.
|
|
3261
|
+
*
|
|
3262
|
+
* Operations that also emit intermediate progress events should include a
|
|
3263
|
+
* `progress` field with the progress payload type.
|
|
3264
|
+
*/
|
|
3228
3265
|
interface RequestRegistry {
|
|
3229
3266
|
GET_CHART_OF_ACCOUNTS: {
|
|
3230
3267
|
payload: Omit<GetCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGetData, 'url'>;
|
|
@@ -3410,6 +3447,10 @@ interface RequestRegistry {
|
|
|
3410
3447
|
payload: Omit<GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetData, 'url'>;
|
|
3411
3448
|
data: GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetResponse;
|
|
3412
3449
|
};
|
|
3450
|
+
INVALIDATE_CACHE: {
|
|
3451
|
+
payload: InvalidateCachePayload;
|
|
3452
|
+
data: InvalidateResult;
|
|
3453
|
+
};
|
|
3413
3454
|
GET_CONTEXT: {
|
|
3414
3455
|
payload: GetContextPayload;
|
|
3415
3456
|
data: ContextPayload;
|
|
@@ -3434,6 +3475,53 @@ type RequestHandlers = {
|
|
|
3434
3475
|
} ? (payload: RequestRegistry[K]['payload'], sendProgress: (p: P) => void) => Promise<RequestRegistry[K]['data']> : (payload: RequestRegistry[K]['payload']) => Promise<RequestRegistry[K]['data']>;
|
|
3435
3476
|
};
|
|
3436
3477
|
|
|
3478
|
+
/**
|
|
3479
|
+
* An error that crossed the bridge, carrying a machine-readable {@link code}
|
|
3480
|
+
* alongside the human-readable message.
|
|
3481
|
+
*
|
|
3482
|
+
* The host attaches a `code` to failed responses (e.g. `'RATE_LIMITED'`,
|
|
3483
|
+
* `'FILE_TOO_LARGE'`, `'REQUEST_FAILED'`); the bridge rehydrates it into a
|
|
3484
|
+
* `BridgeError` so app `catch` blocks can branch on `error.code` instead of
|
|
3485
|
+
* string-matching `error.message`. A failed Nominal API call rehydrates as the
|
|
3486
|
+
* {@link HttpBridgeError} subtype, which adds the HTTP `status`.
|
|
3487
|
+
*
|
|
3488
|
+
* @example
|
|
3489
|
+
* ```ts
|
|
3490
|
+
* try {
|
|
3491
|
+
* await bridge.getAccounts({})
|
|
3492
|
+
* } catch (err) {
|
|
3493
|
+
* if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
|
|
3494
|
+
* // back off and retry
|
|
3495
|
+
* }
|
|
3496
|
+
* }
|
|
3497
|
+
* ```
|
|
3498
|
+
*/
|
|
3499
|
+
declare class BridgeError extends Error {
|
|
3500
|
+
readonly code: string;
|
|
3501
|
+
constructor(code: string, message: string);
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* A {@link BridgeError} for a failed Nominal API call. Its `code` is always
|
|
3505
|
+
* `'REQUEST_FAILED'`; the HTTP {@link status} is carried as a structured field
|
|
3506
|
+
* so callers can branch on it (e.g. `err.status === 404`) instead of parsing
|
|
3507
|
+
* the code string.
|
|
3508
|
+
*
|
|
3509
|
+
* @example
|
|
3510
|
+
* ```ts
|
|
3511
|
+
* try {
|
|
3512
|
+
* await bridge.getAccount({ path: { account_id: 'missing' } })
|
|
3513
|
+
* } catch (err) {
|
|
3514
|
+
* if (err instanceof HttpBridgeError && err.status === 404) {
|
|
3515
|
+
* // handle not-found
|
|
3516
|
+
* }
|
|
3517
|
+
* }
|
|
3518
|
+
* ```
|
|
3519
|
+
*/
|
|
3520
|
+
declare class HttpBridgeError extends BridgeError {
|
|
3521
|
+
readonly status: number;
|
|
3522
|
+
constructor(status: number, message: string);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3437
3525
|
interface VibeApiClientOptions {
|
|
3438
3526
|
baseUrl: string;
|
|
3439
3527
|
/**
|
|
@@ -3446,6 +3534,18 @@ interface VibeApiClientOptions {
|
|
|
3446
3534
|
|
|
3447
3535
|
type HostHandlers = Omit<RequestHandlers, 'GET_CONTEXT'>;
|
|
3448
3536
|
type HostContext = Omit<ContextPayload, 'hostVersion'>;
|
|
3537
|
+
/**
|
|
3538
|
+
* Notified after each request resolves — except the `GET_CONTEXT` connect poll,
|
|
3539
|
+
* which is exempt. Used by nom-ui for SWR busting (SSOT §5j).
|
|
3540
|
+
*/
|
|
3541
|
+
interface RequestCompleteInfo {
|
|
3542
|
+
/** The operation type, e.g. `'POST_TASK_OUTPUT'`. */
|
|
3543
|
+
type: string;
|
|
3544
|
+
/** Whether the operation succeeded. */
|
|
3545
|
+
ok: boolean;
|
|
3546
|
+
/** The request payload that was handled. */
|
|
3547
|
+
payload: unknown;
|
|
3548
|
+
}
|
|
3449
3549
|
interface VibeAppHostOptions {
|
|
3450
3550
|
/**
|
|
3451
3551
|
* Iframe origins allowed to talk to this host. Messages from any other origin
|
|
@@ -3467,13 +3567,33 @@ interface VibeAppHostOptions {
|
|
|
3467
3567
|
* `{ baseUrl: '', stripApiPrefix: false }`.
|
|
3468
3568
|
*/
|
|
3469
3569
|
clientConfig?: VibeApiClientOptions;
|
|
3570
|
+
/** Fire-and-forget. Resolve a path / named route and push to the host router. */
|
|
3571
|
+
onNavigate?: (payload: NavigatePayload) => void;
|
|
3572
|
+
/** Fire-and-forget. Collapse/expand the host side nav. */
|
|
3573
|
+
onSetSideNav?: (payload: SetSideNavPayload) => void;
|
|
3574
|
+
/** Fire-and-forget. Switch subsidiary (host owns validation + navigation). */
|
|
3575
|
+
onSwitchSubsidiary?: (payload: SwitchSubsidiaryPayload) => void;
|
|
3576
|
+
/** Awaited. Invalidate host cache by tags/paths. Defaults to a no-op. */
|
|
3577
|
+
onInvalidateCache?: (payload: InvalidateCachePayload) => Promise<InvalidateResult>;
|
|
3578
|
+
/**
|
|
3579
|
+
* Awaited. Real file upload — replaces the built-in stub (DP2). When omitted,
|
|
3580
|
+
* `UPLOAD_FILE` requests reject with a `'NOT_WIRED'` coded error.
|
|
3581
|
+
*/
|
|
3582
|
+
onUpload?: (payload: UploadPayload, sendProgress: (p: UploadProgress) => void) => Promise<UploadResponse>;
|
|
3583
|
+
/** Optional. Notified after each request resolves (except the `GET_CONTEXT` connect poll) — used by nom-ui for SWR busting (SSOT §5j). */
|
|
3584
|
+
onRequestComplete?: (info: RequestCompleteInfo) => void;
|
|
3585
|
+
/** Requests/min before a request trips `RATE_LIMITED` (DP5). Default 60. */
|
|
3586
|
+
rateLimit?: number;
|
|
3470
3587
|
}
|
|
3588
|
+
|
|
3471
3589
|
/**
|
|
3472
3590
|
* Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
|
|
3473
3591
|
* iframe origin(s), a `getContext` callback, the app's base path, and a
|
|
3474
3592
|
* `clientConfig` pointing at the Nominal API; then call {@link VibeAppHost.mount}
|
|
3475
|
-
* to start listening. The handler map for all protocol operations is built
|
|
3476
|
-
* automatically — you do not pass handlers.
|
|
3593
|
+
* to start listening. The handler map for all protocol API operations is built
|
|
3594
|
+
* automatically — you do not pass those handlers. Non-API side effects
|
|
3595
|
+
* (navigation, side nav, subsidiary switch, cache invalidation, upload) are
|
|
3596
|
+
* supplied via the optional injected callbacks.
|
|
3477
3597
|
*
|
|
3478
3598
|
* @example
|
|
3479
3599
|
* ```tsx
|
|
@@ -3483,6 +3603,8 @@ interface VibeAppHostOptions {
|
|
|
3483
3603
|
* appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
3484
3604
|
* getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
|
|
3485
3605
|
* clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
|
|
3606
|
+
* onNavigate: (p) => router.push(resolve(p)),
|
|
3607
|
+
* onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
|
|
3486
3608
|
* })
|
|
3487
3609
|
*
|
|
3488
3610
|
* useEffect(() => {
|
|
@@ -3501,16 +3623,35 @@ interface VibeAppHostOptions {
|
|
|
3501
3623
|
declare class VibeAppHost {
|
|
3502
3624
|
private readonly trustedOrigins;
|
|
3503
3625
|
private readonly handlers;
|
|
3626
|
+
private readonly commandHandlers;
|
|
3504
3627
|
private readonly getContext;
|
|
3505
3628
|
private readonly appBasePath;
|
|
3629
|
+
private readonly onRequestComplete?;
|
|
3630
|
+
private readonly rateLimiter;
|
|
3506
3631
|
private readonly boundHandleMessage;
|
|
3507
3632
|
private readonly boundHandlePopState;
|
|
3508
3633
|
private iframeWindow;
|
|
3634
|
+
/**
|
|
3635
|
+
* Source windows we've already logged a successful handshake for. The bridge
|
|
3636
|
+
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
3637
|
+
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
3638
|
+
*/
|
|
3639
|
+
private readonly handshakeLoggedWindows;
|
|
3509
3640
|
private readonly kindHandlers;
|
|
3510
3641
|
private dispatchCommand;
|
|
3511
|
-
|
|
3642
|
+
/**
|
|
3643
|
+
* Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
|
|
3644
|
+
* guard, item `l`); navigation/UI side effects are injected by the consumer,
|
|
3645
|
+
* falling back to warn-and-ignore (DP4).
|
|
3646
|
+
*/
|
|
3647
|
+
private buildCommandHandlers;
|
|
3648
|
+
/**
|
|
3649
|
+
* Requests: the built-in API map (hey-api), plus the injected/awaited side
|
|
3650
|
+
* effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
|
|
3651
|
+
* error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
|
|
3652
|
+
*/
|
|
3512
3653
|
private initializeHandlers;
|
|
3513
|
-
constructor(
|
|
3654
|
+
constructor(opts: VibeAppHostOptions);
|
|
3514
3655
|
/**
|
|
3515
3656
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
3516
3657
|
* Returns a cleanup function to call on unmount.
|
|
@@ -3535,4 +3676,4 @@ declare class VibeAppHost {
|
|
|
3535
3676
|
private respond;
|
|
3536
3677
|
}
|
|
3537
3678
|
|
|
3538
|
-
export { type ContextPayload, type HostContext, type HostHandlers, type RequestHandlers, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
|
3679
|
+
export { BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.2.0";
|
|
3
3
|
|
|
4
4
|
// ../protocol-types/dist/index.js
|
|
5
|
+
function normalizeSubroute(subroute) {
|
|
6
|
+
const normalized = subroute.startsWith("/") ? subroute : `/${subroute}`;
|
|
7
|
+
const path = normalized.split(/[?#]/)[0];
|
|
8
|
+
let decoded = path;
|
|
9
|
+
let prev;
|
|
10
|
+
do {
|
|
11
|
+
prev = decoded;
|
|
12
|
+
decoded = decoded.replace(/%25/gi, "%").replace(/%2e/gi, ".").replace(/%2f/gi, "/");
|
|
13
|
+
} while (decoded !== prev);
|
|
14
|
+
if (decoded.split("/").some((segment) => segment === "..")) return null;
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
5
17
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
6
18
|
var PROTOCOL_VERSION = 1;
|
|
7
19
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
@@ -18,6 +30,20 @@ function isBridgeMessage(data) {
|
|
|
18
30
|
if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
|
|
19
31
|
return true;
|
|
20
32
|
}
|
|
33
|
+
var BridgeError = class extends Error {
|
|
34
|
+
constructor(code, message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.name = "BridgeError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var HttpBridgeError = class extends BridgeError {
|
|
41
|
+
constructor(status, message) {
|
|
42
|
+
super("REQUEST_FAILED", message);
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.name = "HttpBridgeError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
21
47
|
|
|
22
48
|
// ../api-client/dist/index.js
|
|
23
49
|
import {
|
|
@@ -352,10 +378,6 @@ var getSubsidiaryParentCurrenciesApiTenancySubsidiarySubsidiaryIdParentCurrencie
|
|
|
352
378
|
};
|
|
353
379
|
function createVibeApiClient({ baseUrl, stripApiPrefix }) {
|
|
354
380
|
const client2 = createClient2(createConfig2({ baseUrl }));
|
|
355
|
-
client2.interceptors.request.use((request) => {
|
|
356
|
-
console.log("[vibe-api-client] request url:", request.url);
|
|
357
|
-
return request;
|
|
358
|
-
});
|
|
359
381
|
if (stripApiPrefix) {
|
|
360
382
|
client2.interceptors.request.use(
|
|
361
383
|
(request) => new Request(request.url.replace(`${baseUrl}/api/`, `${baseUrl}/`), request)
|
|
@@ -364,16 +386,28 @@ function createVibeApiClient({ baseUrl, stripApiPrefix }) {
|
|
|
364
386
|
return client2;
|
|
365
387
|
}
|
|
366
388
|
|
|
367
|
-
// src/
|
|
389
|
+
// src/handlers/call.ts
|
|
368
390
|
async function call(fn, payload, client2) {
|
|
369
|
-
const { data } = await fn({ ...payload, client: client2 });
|
|
370
|
-
if (data == null)
|
|
391
|
+
const { data, response } = await fn({ ...payload, client: client2 });
|
|
392
|
+
if (data == null) {
|
|
393
|
+
if (response && !response.ok) {
|
|
394
|
+
throw new HttpBridgeError(
|
|
395
|
+
response.status,
|
|
396
|
+
`API request failed with status ${response.status}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (!response) {
|
|
400
|
+
throw new Error("API request produced no response");
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
371
404
|
return data;
|
|
372
405
|
}
|
|
373
|
-
|
|
374
|
-
|
|
406
|
+
|
|
407
|
+
// src/handlers/accounting.ts
|
|
408
|
+
function accountingHandlers(client2) {
|
|
375
409
|
return {
|
|
376
|
-
//
|
|
410
|
+
// Chart of accounts
|
|
377
411
|
GET_CHART_OF_ACCOUNTS: (p) => call(getCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGet, p, client2),
|
|
378
412
|
GET_COA_TREE: (p) => call(getCoaTreeApiAccountingSubsidiarySubsidiaryIdCoaGet, p, client2),
|
|
379
413
|
GET_COA_FLAT_SIMPLE: (p) => call(
|
|
@@ -398,7 +432,7 @@ function createHandlerMap(options) {
|
|
|
398
432
|
),
|
|
399
433
|
GET_ACCOUNTS: (p) => call(getAccountsApiAccountingAccountGet, p, client2),
|
|
400
434
|
GET_ACCOUNT: (p) => call(getAccountApiAccountingAccountAccountIdGet, p, client2),
|
|
401
|
-
//
|
|
435
|
+
// Exchange rates
|
|
402
436
|
GET_CONVERSION_RATES: (p) => call(getConversionRateApiAccountingCurrencyCurrencyConversionRatesGet, p, client2),
|
|
403
437
|
GET_EFFECTIVE_EXCHANGE_RATE: (p) => call(getEffectiveExchangeRateApiAccountingConsolidationExchangeRateGet, p, client2),
|
|
404
438
|
GET_EXCHANGE_RATE_BY_DATE: (p) => call(
|
|
@@ -406,7 +440,7 @@ function createHandlerMap(options) {
|
|
|
406
440
|
p,
|
|
407
441
|
client2
|
|
408
442
|
),
|
|
409
|
-
//
|
|
443
|
+
// Dimensions
|
|
410
444
|
GET_DIMENSIONS: (p) => call(getDimensionsApiAccountingDimensionGet, p, client2),
|
|
411
445
|
GET_DIMENSION_VALUES: (p) => call(getDimensionValuesApiAccountingDimensionValueGet, p, client2),
|
|
412
446
|
GET_DIMENSION_VALUES_HIERARCHICAL: (p) => call(
|
|
@@ -419,7 +453,7 @@ function createHandlerMap(options) {
|
|
|
419
453
|
p,
|
|
420
454
|
client2
|
|
421
455
|
),
|
|
422
|
-
//
|
|
456
|
+
// Journal entries
|
|
423
457
|
GET_JOURNAL_ENTRIES: (p) => call(
|
|
424
458
|
getSubsidiaryJournalApiAccountingSubsidiarySubsidiaryIdJournalEntryGet,
|
|
425
459
|
p,
|
|
@@ -434,8 +468,14 @@ function createHandlerMap(options) {
|
|
|
434
468
|
getJournalEntryApiAccountingSubsidiarySubsidiaryIdJournalEntryJournalEntryIdGet,
|
|
435
469
|
p,
|
|
436
470
|
client2
|
|
437
|
-
)
|
|
438
|
-
|
|
471
|
+
)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/handlers/activity.ts
|
|
476
|
+
function activityHandlers(client2) {
|
|
477
|
+
return {
|
|
478
|
+
// Period instances
|
|
439
479
|
GET_PERIODS: (p) => call(getPeriodInstancesApiActivityPeriodInstanceGet, p, client2),
|
|
440
480
|
GET_PERIOD_INSTANCE: (p) => call(getPeriodInstanceApiActivityPeriodInstancePeriodInstanceIdGet, p, client2),
|
|
441
481
|
GET_PERIOD_INSTANCE_BY_SLUG: (p) => call(
|
|
@@ -448,14 +488,14 @@ function createHandlerMap(options) {
|
|
|
448
488
|
p,
|
|
449
489
|
client2
|
|
450
490
|
),
|
|
451
|
-
// Activity
|
|
491
|
+
// Activity definitions
|
|
452
492
|
GET_ACTIVITY_DEFINITIONS: (p) => call(getActivityDefinitionsApiActivityActivityDefinitionGet, p, client2),
|
|
453
493
|
GET_ACTIVITY_DEFINITION: (p) => call(
|
|
454
494
|
getActivityDefinitionByIdApiActivityActivityDefinitionActivityDefinitionIdGet,
|
|
455
495
|
p,
|
|
456
496
|
client2
|
|
457
497
|
),
|
|
458
|
-
// Activity
|
|
498
|
+
// Activity instances
|
|
459
499
|
GET_ACTIVITY_INSTANCES: (p) => call(getActivityInstancesApiActivityActivityInstanceGet, p, client2),
|
|
460
500
|
GET_ACTIVITY_INSTANCE: (p) => call(getActivityInstanceApiActivityActivityInstanceActivityInstanceIdGet, p, client2),
|
|
461
501
|
GET_ACTIVITY_INSTANCE_BY_PERIOD: (p) => call(
|
|
@@ -473,28 +513,43 @@ function createHandlerMap(options) {
|
|
|
473
513
|
p,
|
|
474
514
|
client2
|
|
475
515
|
),
|
|
476
|
-
//
|
|
516
|
+
// Task definitions
|
|
477
517
|
GET_TASK_DEFINITIONS: (p) => call(getAllTasksDefinitionsApiActivityTaskDefinitionGet, p, client2),
|
|
478
518
|
GET_TASK_DEFINITION: (p) => call(getTaskDefinitionByIdApiActivityTaskDefinitionTaskDefinitionIdGet, p, client2),
|
|
479
519
|
CREATE_TASK_DEFINITION: (p) => call(createTaskDefinitionApiActivityTaskDefinitionPost, p, client2),
|
|
480
520
|
UPDATE_TASK_DEFINITION: (p) => call(updateTaskDefinitionApiActivityTaskDefinitionTaskDefinitionIdPut, p, client2),
|
|
481
521
|
GET_TASK_DEFINITIONS_BY_FILTER: (p) => call(getAllTasksDefinitionsByFilterApiActivityTaskDefinitionQueryPost, p, client2),
|
|
482
|
-
//
|
|
522
|
+
// Task instances
|
|
483
523
|
GET_TASK_INSTANCES: (p) => call(getAllTaskInstancesApiActivityTaskInstanceGet, p, client2),
|
|
484
524
|
GET_TASK_INSTANCE: (p) => call(getTaskInstanceByIdApiActivityTaskInstanceTaskInstanceIdGet, p, client2),
|
|
485
525
|
POST_TASK_OUTPUT: (p) => call(updateTaskInstanceApiActivityTaskInstanceTaskInstanceIdPut, p, client2),
|
|
486
|
-
GET_TASK_INSTANCES_BY_FILTER: (p) => call(getAllTaskInstancesByFilterApiActivityTaskInstanceQueryPost, p, client2)
|
|
487
|
-
|
|
526
|
+
GET_TASK_INSTANCES_BY_FILTER: (p) => call(getAllTaskInstancesByFilterApiActivityTaskInstanceQueryPost, p, client2)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/handlers/audit-trail.ts
|
|
531
|
+
function auditTrailHandlers(client2) {
|
|
532
|
+
return {
|
|
488
533
|
GET_AUDIT_EVENTS: (p) => call(getEventsApiAuditTrailEventGet, p, client2),
|
|
489
|
-
GET_ENTITY_AUDIT_EVENTS: (p) => call(getEntityEventsApiAuditTrailEventEntityTargetTypeTargetIdPost, p, client2)
|
|
490
|
-
|
|
534
|
+
GET_ENTITY_AUDIT_EVENTS: (p) => call(getEntityEventsApiAuditTrailEventEntityTargetTypeTargetIdPost, p, client2)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/handlers/period-manager.ts
|
|
539
|
+
function periodManagerHandlers(client2) {
|
|
540
|
+
return {
|
|
491
541
|
GET_FISCAL_CALENDARS: (p) => call(
|
|
492
542
|
getFiscalCalendarsBySubsidiaryApiPeriodManagerFiscalCalendarBySubsidiarySubsidiaryIdGet,
|
|
493
543
|
p,
|
|
494
544
|
client2
|
|
495
545
|
),
|
|
496
|
-
GET_FISCAL_CALENDAR: (p) => call(getFiscalCalendarByIdApiPeriodManagerFiscalCalendarCalendarIdGet, p, client2)
|
|
497
|
-
|
|
546
|
+
GET_FISCAL_CALENDAR: (p) => call(getFiscalCalendarByIdApiPeriodManagerFiscalCalendarCalendarIdGet, p, client2)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/handlers/tenancy.ts
|
|
551
|
+
function tenancyHandlers(client2) {
|
|
552
|
+
return {
|
|
498
553
|
GET_SUBSIDIARIES: (p) => call(getAllSubsidiariesApiTenancySubsidiaryGet, p, client2),
|
|
499
554
|
GET_SUBSIDIARY: (p) => call(getSubsidiaryApiTenancySubsidiarySubsidiaryIdGet, p, client2),
|
|
500
555
|
GET_SUBSIDIARY_PARENT_CURRENCIES: (p) => call(
|
|
@@ -506,46 +561,115 @@ function createHandlerMap(options) {
|
|
|
506
561
|
};
|
|
507
562
|
}
|
|
508
563
|
|
|
509
|
-
// src/
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
564
|
+
// src/handlerMap.ts
|
|
565
|
+
function createHandlerMap(options) {
|
|
566
|
+
const client2 = createVibeApiClient(options);
|
|
567
|
+
return {
|
|
568
|
+
...accountingHandlers(client2),
|
|
569
|
+
...activityHandlers(client2),
|
|
570
|
+
...auditTrailHandlers(client2),
|
|
571
|
+
...periodManagerHandlers(client2),
|
|
572
|
+
...tenancyHandlers(client2)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/rateLimiter.ts
|
|
577
|
+
var DEFAULT_RATE_LIMIT = 60;
|
|
578
|
+
function createRateLimiter(limit = DEFAULT_RATE_LIMIT, windowMs = 6e4) {
|
|
579
|
+
return { timestamps: [], limit, windowMs };
|
|
580
|
+
}
|
|
581
|
+
function checkRateLimit(state) {
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
const cutoff = now - state.windowMs;
|
|
584
|
+
state.timestamps = state.timestamps.filter((t) => t > cutoff);
|
|
585
|
+
if (state.timestamps.length >= state.limit) {
|
|
586
|
+
return false;
|
|
515
587
|
}
|
|
516
|
-
|
|
588
|
+
state.timestamps.push(now);
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/unwired.ts
|
|
593
|
+
function warnUnwired(command) {
|
|
594
|
+
return () => {
|
|
595
|
+
console.warn(`[vibe-host] Received "${command}" command but no handler is wired \u2014 ignoring.`);
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function rejectUnwired(op) {
|
|
599
|
+
return () => Promise.reject(
|
|
600
|
+
new BridgeError("NOT_WIRED", `[vibe-host] Received "${op}" request but no handler is wired.`)
|
|
601
|
+
);
|
|
517
602
|
}
|
|
518
603
|
|
|
519
604
|
// src/VibeAppHost.ts
|
|
520
605
|
var VibeAppHost = class {
|
|
521
|
-
constructor(
|
|
606
|
+
constructor(opts) {
|
|
522
607
|
this.iframeWindow = null;
|
|
608
|
+
/**
|
|
609
|
+
* Source windows we've already logged a successful handshake for. The bridge
|
|
610
|
+
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
611
|
+
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
612
|
+
*/
|
|
613
|
+
this.handshakeLoggedWindows = /* @__PURE__ */ new WeakSet();
|
|
523
614
|
this.kindHandlers = {
|
|
524
615
|
request: (msg, source, origin) => this.handleRequest(msg, source, origin),
|
|
525
616
|
command: (msg) => this.dispatchCommand(msg)
|
|
526
617
|
};
|
|
527
|
-
|
|
528
|
-
REPORT_SUBROUTE: (payload) => {
|
|
529
|
-
const newPath = payload.subroute === "/" ? this.appBasePath : `${this.appBasePath}${payload.subroute}`;
|
|
530
|
-
const method = payload.replace ? "replaceState" : "pushState";
|
|
531
|
-
window.history[method](null, "", newPath);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
618
|
+
const { trustedOrigins, getContext, appBasePath, clientConfig } = opts;
|
|
534
619
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
535
620
|
this.getContext = getContext;
|
|
536
621
|
this.appBasePath = appBasePath;
|
|
622
|
+
this.onRequestComplete = opts.onRequestComplete;
|
|
623
|
+
this.rateLimiter = createRateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
|
|
537
624
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
538
625
|
this.boundHandlePopState = this.handlePopState.bind(this);
|
|
539
|
-
this.handlers = this.initializeHandlers(
|
|
626
|
+
this.handlers = this.initializeHandlers(
|
|
627
|
+
clientConfig || { baseUrl: "", stripApiPrefix: false },
|
|
628
|
+
opts
|
|
629
|
+
);
|
|
630
|
+
this.commandHandlers = this.buildCommandHandlers(opts);
|
|
540
631
|
}
|
|
541
632
|
dispatchCommand(msg) {
|
|
542
633
|
const handler = this.commandHandlers[msg.type];
|
|
543
|
-
handler
|
|
634
|
+
if (!handler) {
|
|
635
|
+
console.warn(`[vibe-host] Received unknown command "${msg.type}" \u2014 ignoring.`);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
handler(msg.payload);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
console.warn(`[vibe-host] Command "${msg.type}" handler threw \u2014 ignoring.`, err);
|
|
642
|
+
}
|
|
544
643
|
}
|
|
545
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
|
|
646
|
+
* guard, item `l`); navigation/UI side effects are injected by the consumer,
|
|
647
|
+
* falling back to warn-and-ignore (DP4).
|
|
648
|
+
*/
|
|
649
|
+
buildCommandHandlers(opts) {
|
|
650
|
+
return {
|
|
651
|
+
REPORT_SUBROUTE: (payload) => {
|
|
652
|
+
const safe = normalizeSubroute(payload.subroute);
|
|
653
|
+
if (safe === null) return;
|
|
654
|
+
const newPath = safe === "/" ? this.appBasePath : `${this.appBasePath}${safe}`;
|
|
655
|
+
const method = payload.replace ? "replaceState" : "pushState";
|
|
656
|
+
window.history[method](null, "", newPath);
|
|
657
|
+
},
|
|
658
|
+
NAVIGATE: opts.onNavigate ?? warnUnwired("NAVIGATE"),
|
|
659
|
+
SET_SIDE_NAV: opts.onSetSideNav ?? warnUnwired("SET_SIDE_NAV"),
|
|
660
|
+
SWITCH_SUBSIDIARY: opts.onSwitchSubsidiary ?? warnUnwired("SWITCH_SUBSIDIARY")
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Requests: the built-in API map (hey-api), plus the injected/awaited side
|
|
665
|
+
* effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
|
|
666
|
+
* error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
|
|
667
|
+
*/
|
|
668
|
+
initializeHandlers(clientConfig, opts) {
|
|
546
669
|
return {
|
|
547
670
|
...createHandlerMap(clientConfig),
|
|
548
|
-
UPLOAD_FILE:
|
|
671
|
+
UPLOAD_FILE: opts.onUpload ?? rejectUnwired("UPLOAD_FILE"),
|
|
672
|
+
INVALIDATE_CACHE: opts.onInvalidateCache ?? (async () => ({ invalidated: false }))
|
|
549
673
|
};
|
|
550
674
|
}
|
|
551
675
|
/**
|
|
@@ -617,12 +741,33 @@ var VibeAppHost = class {
|
|
|
617
741
|
async handleRequest(msg, source, origin) {
|
|
618
742
|
const { requestId, type } = msg;
|
|
619
743
|
if (type === "GET_CONTEXT") {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
744
|
+
try {
|
|
745
|
+
const ctx = { ...this.getContext(), hostVersion: version };
|
|
746
|
+
if (!this.handshakeLoggedWindows.has(source)) {
|
|
747
|
+
this.handshakeLoggedWindows.add(source);
|
|
748
|
+
const bridgeVersion = msg.payload?.bridgeVersion ?? "unknown";
|
|
749
|
+
console.log(
|
|
750
|
+
`[vibe-host] Vibe App connected \u2014 bridge: ${bridgeVersion}, host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data: ctx });
|
|
754
|
+
} catch (err) {
|
|
755
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
756
|
+
const code = err instanceof BridgeError ? err.code : "CONTEXT_ERROR";
|
|
757
|
+
this.respond(source, origin, { kind: "response", type, requestId, ok: false, error, code });
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (!checkRateLimit(this.rateLimiter)) {
|
|
762
|
+
this.respond(source, origin, {
|
|
763
|
+
kind: "response",
|
|
764
|
+
type,
|
|
765
|
+
requestId,
|
|
766
|
+
ok: false,
|
|
767
|
+
error: `Too many requests. Limit is ${this.rateLimiter.limit} per minute.`,
|
|
768
|
+
code: "RATE_LIMITED"
|
|
769
|
+
});
|
|
770
|
+
this.onRequestComplete?.({ type, ok: false, payload: msg.payload });
|
|
626
771
|
return;
|
|
627
772
|
}
|
|
628
773
|
const sendProgress = (payload) => {
|
|
@@ -640,9 +785,21 @@ var VibeAppHost = class {
|
|
|
640
785
|
const handler = this.handlers[type];
|
|
641
786
|
const data = await handler(msg.payload, sendProgress);
|
|
642
787
|
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data });
|
|
788
|
+
this.onRequestComplete?.({ type, ok: true, payload: msg.payload });
|
|
643
789
|
} catch (err) {
|
|
644
790
|
const error = err instanceof Error ? err.message : String(err);
|
|
645
|
-
|
|
791
|
+
const code = err instanceof BridgeError ? err.code : void 0;
|
|
792
|
+
const status = err instanceof HttpBridgeError ? err.status : void 0;
|
|
793
|
+
this.respond(source, origin, {
|
|
794
|
+
kind: "response",
|
|
795
|
+
type,
|
|
796
|
+
requestId,
|
|
797
|
+
ok: false,
|
|
798
|
+
error,
|
|
799
|
+
code,
|
|
800
|
+
status
|
|
801
|
+
});
|
|
802
|
+
this.onRequestComplete?.({ type, ok: false, payload: msg.payload });
|
|
646
803
|
}
|
|
647
804
|
}
|
|
648
805
|
respond(target, origin, response) {
|
|
@@ -653,5 +810,7 @@ var VibeAppHost = class {
|
|
|
653
810
|
}
|
|
654
811
|
};
|
|
655
812
|
export {
|
|
813
|
+
BridgeError,
|
|
814
|
+
HttpBridgeError,
|
|
656
815
|
VibeAppHost
|
|
657
816
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Host-side SDK for embedding Nominal Vibe Apps — receives bridge requests over a typed postMessage protocol and dispatches them to Nominal APIs (used by nom-ui).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
|