@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/dist/index.js CHANGED
@@ -1,7 +1,19 @@
1
1
  // package.json
2
- var version = "0.0.1";
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/handlerMap.ts
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) throw new Error("API returned no data");
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
- function createHandlerMap(options) {
374
- const client2 = createVibeApiClient(options);
406
+
407
+ // src/handlers/accounting.ts
408
+ function accountingHandlers(client2) {
375
409
  return {
376
- // Accounting — chart of accounts
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
- // Accounting — exchange rates
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
- // Accounting — dimensions
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
- // Accounting — journal entries
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
- // Activity — period instances
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 — activity definitions
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 — activity instances
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
- // Activity — task definitions
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
- // Activity — task instances
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
- // Audit trail
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
- // Period manager
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
- // Tenancy
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/uploadHandler.ts
510
- async function uploadHandler(payload, sendProgress) {
511
- const attachmentId = crypto.randomUUID();
512
- for (const progress of [25, 50, 75, 100]) {
513
- await new Promise((r) => setTimeout(r, 400));
514
- sendProgress({ attachmentId, progress });
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
- return { attachmentId, name: payload.fileName };
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({ trustedOrigins, getContext, appBasePath, clientConfig }) {
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
- this.commandHandlers = {
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(clientConfig || { baseUrl: "", stripApiPrefix: false });
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(msg.payload);
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
- initializeHandlers(clientConfig) {
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: uploadHandler
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
- const ctx = { ...this.getContext(), hostVersion: version };
621
- const bridgeVersion = msg.payload?.bridgeVersion ?? "unknown";
622
- console.log(
623
- `[vibe-host] Vibe App connected \u2014 bridge: ${bridgeVersion}, host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
624
- );
625
- this.respond(source, origin, { kind: "response", type, requestId, ok: true, data: ctx });
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
- this.respond(source, origin, { kind: "response", type, requestId, ok: false, error });
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,18 +1,8 @@
1
1
  {
2
2
  "name": "@nominalso/vibe-host",
3
- "version": "0.0.1",
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
- "keywords": [
7
- "nominal",
8
- "vibe-apps",
9
- "vibe-app",
10
- "sdk",
11
- "iframe",
12
- "postmessage",
13
- "host",
14
- "embed"
15
- ],
16
6
  "homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
17
7
  "bugs": {
18
8
  "url": "https://github.com/nominalso/vibe-apps-sdk/issues"