@nominalso/vibe-host 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -11
- package/dist/index.cjs +213 -52
- package/dist/index.d.cts +194 -12
- package/dist/index.d.ts +194 -12
- package/dist/index.js +211 -52
- package/package.json +1 -11
package/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,7 +3534,24 @@ 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 {
|
|
3550
|
+
/**
|
|
3551
|
+
* Iframe origins allowed to talk to this host. Messages from any other origin
|
|
3552
|
+
* are ignored, and context/pushes are only sent to these origins. Must match
|
|
3553
|
+
* the Vibe App's origin exactly (e.g. `'https://my-vibe-app.lovable.app'`).
|
|
3554
|
+
*/
|
|
3450
3555
|
trustedOrigins: string[];
|
|
3451
3556
|
/** Returns the current context to send to the iframe on connect. */
|
|
3452
3557
|
getContext: () => HostContext;
|
|
@@ -3455,21 +3560,98 @@ interface VibeAppHostOptions {
|
|
|
3455
3560
|
* Used to sync the browser URL with the iframe's internal subroute.
|
|
3456
3561
|
*/
|
|
3457
3562
|
appBasePath: string;
|
|
3563
|
+
/**
|
|
3564
|
+
* Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy
|
|
3565
|
+
* route). The host provides handlers for every protocol operation out of the
|
|
3566
|
+
* box; this just tells them where to send requests. Defaults to
|
|
3567
|
+
* `{ baseUrl: '', stripApiPrefix: false }`.
|
|
3568
|
+
*/
|
|
3458
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;
|
|
3459
3587
|
}
|
|
3588
|
+
|
|
3589
|
+
/**
|
|
3590
|
+
* Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
|
|
3591
|
+
* iframe origin(s), a `getContext` callback, the app's base path, and a
|
|
3592
|
+
* `clientConfig` pointing at the Nominal API; then call {@link VibeAppHost.mount}
|
|
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.
|
|
3597
|
+
*
|
|
3598
|
+
* @example
|
|
3599
|
+
* ```tsx
|
|
3600
|
+
* // In a React (nom-ui) component:
|
|
3601
|
+
* const host = new VibeAppHost({
|
|
3602
|
+
* trustedOrigins: ['https://my-vibe-app.lovable.app'],
|
|
3603
|
+
* appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
3604
|
+
* getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
|
|
3605
|
+
* clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
|
|
3606
|
+
* onNavigate: (p) => router.push(resolve(p)),
|
|
3607
|
+
* onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
|
|
3608
|
+
* })
|
|
3609
|
+
*
|
|
3610
|
+
* useEffect(() => {
|
|
3611
|
+
* const iframe = iframeRef.current
|
|
3612
|
+
* if (!iframe) return
|
|
3613
|
+
* const unmount = host.mount() // listen before the iframe loads
|
|
3614
|
+
* const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
|
|
3615
|
+
* iframe.addEventListener('load', onLoad)
|
|
3616
|
+
* return () => {
|
|
3617
|
+
* iframe.removeEventListener('load', onLoad)
|
|
3618
|
+
* unmount()
|
|
3619
|
+
* }
|
|
3620
|
+
* }, [host])
|
|
3621
|
+
* ```
|
|
3622
|
+
*/
|
|
3460
3623
|
declare class VibeAppHost {
|
|
3461
3624
|
private readonly trustedOrigins;
|
|
3462
3625
|
private readonly handlers;
|
|
3626
|
+
private readonly commandHandlers;
|
|
3463
3627
|
private readonly getContext;
|
|
3464
3628
|
private readonly appBasePath;
|
|
3629
|
+
private readonly onRequestComplete?;
|
|
3630
|
+
private readonly rateLimiter;
|
|
3465
3631
|
private readonly boundHandleMessage;
|
|
3466
3632
|
private readonly boundHandlePopState;
|
|
3467
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;
|
|
3468
3640
|
private readonly kindHandlers;
|
|
3469
3641
|
private dispatchCommand;
|
|
3470
|
-
|
|
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
|
+
*/
|
|
3471
3653
|
private initializeHandlers;
|
|
3472
|
-
constructor(
|
|
3654
|
+
constructor(opts: VibeAppHostOptions);
|
|
3473
3655
|
/**
|
|
3474
3656
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
3475
3657
|
* Returns a cleanup function to call on unmount.
|
|
@@ -3494,4 +3676,4 @@ declare class VibeAppHost {
|
|
|
3494
3676
|
private respond;
|
|
3495
3677
|
}
|
|
3496
3678
|
|
|
3497
|
-
export { type HostContext, type HostHandlers, type RequestHandlers, 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,7 +3534,24 @@ 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 {
|
|
3550
|
+
/**
|
|
3551
|
+
* Iframe origins allowed to talk to this host. Messages from any other origin
|
|
3552
|
+
* are ignored, and context/pushes are only sent to these origins. Must match
|
|
3553
|
+
* the Vibe App's origin exactly (e.g. `'https://my-vibe-app.lovable.app'`).
|
|
3554
|
+
*/
|
|
3450
3555
|
trustedOrigins: string[];
|
|
3451
3556
|
/** Returns the current context to send to the iframe on connect. */
|
|
3452
3557
|
getContext: () => HostContext;
|
|
@@ -3455,21 +3560,98 @@ interface VibeAppHostOptions {
|
|
|
3455
3560
|
* Used to sync the browser URL with the iframe's internal subroute.
|
|
3456
3561
|
*/
|
|
3457
3562
|
appBasePath: string;
|
|
3563
|
+
/**
|
|
3564
|
+
* Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy
|
|
3565
|
+
* route). The host provides handlers for every protocol operation out of the
|
|
3566
|
+
* box; this just tells them where to send requests. Defaults to
|
|
3567
|
+
* `{ baseUrl: '', stripApiPrefix: false }`.
|
|
3568
|
+
*/
|
|
3458
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;
|
|
3459
3587
|
}
|
|
3588
|
+
|
|
3589
|
+
/**
|
|
3590
|
+
* Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
|
|
3591
|
+
* iframe origin(s), a `getContext` callback, the app's base path, and a
|
|
3592
|
+
* `clientConfig` pointing at the Nominal API; then call {@link VibeAppHost.mount}
|
|
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.
|
|
3597
|
+
*
|
|
3598
|
+
* @example
|
|
3599
|
+
* ```tsx
|
|
3600
|
+
* // In a React (nom-ui) component:
|
|
3601
|
+
* const host = new VibeAppHost({
|
|
3602
|
+
* trustedOrigins: ['https://my-vibe-app.lovable.app'],
|
|
3603
|
+
* appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
3604
|
+
* getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
|
|
3605
|
+
* clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
|
|
3606
|
+
* onNavigate: (p) => router.push(resolve(p)),
|
|
3607
|
+
* onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
|
|
3608
|
+
* })
|
|
3609
|
+
*
|
|
3610
|
+
* useEffect(() => {
|
|
3611
|
+
* const iframe = iframeRef.current
|
|
3612
|
+
* if (!iframe) return
|
|
3613
|
+
* const unmount = host.mount() // listen before the iframe loads
|
|
3614
|
+
* const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
|
|
3615
|
+
* iframe.addEventListener('load', onLoad)
|
|
3616
|
+
* return () => {
|
|
3617
|
+
* iframe.removeEventListener('load', onLoad)
|
|
3618
|
+
* unmount()
|
|
3619
|
+
* }
|
|
3620
|
+
* }, [host])
|
|
3621
|
+
* ```
|
|
3622
|
+
*/
|
|
3460
3623
|
declare class VibeAppHost {
|
|
3461
3624
|
private readonly trustedOrigins;
|
|
3462
3625
|
private readonly handlers;
|
|
3626
|
+
private readonly commandHandlers;
|
|
3463
3627
|
private readonly getContext;
|
|
3464
3628
|
private readonly appBasePath;
|
|
3629
|
+
private readonly onRequestComplete?;
|
|
3630
|
+
private readonly rateLimiter;
|
|
3465
3631
|
private readonly boundHandleMessage;
|
|
3466
3632
|
private readonly boundHandlePopState;
|
|
3467
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;
|
|
3468
3640
|
private readonly kindHandlers;
|
|
3469
3641
|
private dispatchCommand;
|
|
3470
|
-
|
|
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
|
+
*/
|
|
3471
3653
|
private initializeHandlers;
|
|
3472
|
-
constructor(
|
|
3654
|
+
constructor(opts: VibeAppHostOptions);
|
|
3473
3655
|
/**
|
|
3474
3656
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
3475
3657
|
* Returns a cleanup function to call on unmount.
|
|
@@ -3494,4 +3676,4 @@ declare class VibeAppHost {
|
|
|
3494
3676
|
private respond;
|
|
3495
3677
|
}
|
|
3496
3678
|
|
|
3497
|
-
export { type HostContext, type HostHandlers, type RequestHandlers, 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 };
|