experimentalsince 0.4.0-pre.12

liff

LIFF shell binding — initLiffApp() establishes the shell, enforces LINE Login, reads the ID token for on-oidc unlock, and ingests a share-link deep link.

@noy-db/in-liff · pnpm add @noy-db/hub @noy-db/in-liff The boot layer for a noy-db portal opened from LINE.

What it does

One SPA, three shells. initLiffApp() works out which one it is running in ('liff' | 'browser' | 'pwa'), enforces LINE Login, reads the ID token that on-oidc needs for its split-key unlock, and ingests a share-link deep link from the current location — in one call.

The LIFF SDK is injected, never a dependency. Every entry point takes a LiffLike — the six-member structural slice this package actually calls — so your app passes the real liff global and your tests pass a fake.

import liff from '@line/liff'
import { initLiffApp, getFreshIdToken, openExternal } from '@noy-db/in-liff'

const ctx = await initLiffApp({ liff, liffId: import.meta.env.VITE_LIFF_ID })
// → { shell, loggedIn, idToken, inClient, deepLink }

The token lifecycle is the part to get right

LINE ID tokens live about an hour and there is no silent refresh. Expiry is detected client-side from the JWT exp — no network call, no signature check, because verification is the key-connector server's job under on-oidc's documented split.

The consequence is the useful bit: an unlocked noy-db session survives token expiry. The token is only needed again at the next serverHalf fetch, so refresh it exactly there rather than on a timer:

// right before an on-oidc serverHalf fetch — re-logs in on expiry (navigates away)
const token = getFreshIdToken(liff)
// or { onExpired: 'throw' } if you'd rather handle it yourself

Escaping the in-app WebView

openExternal(liff, url) breaks out of LINE's WebView. What that does depends on the platform, and the difference matters because storage does not travel:

PlatformWhat happens
AndroidIn-scope links open the installed PWA directly (WebAPK link capture), which shares Chrome's origin storage.
iOSAlways opens Safari. The installed home-screen app cannot be targeted by URL and has its own storage partition — show an interstitial for the manual hop.
AllLINE's WebView storage is always isolated. The new shell starts empty and re-enrolls through the custodian re-invite — a ceremony, not a data transfer.
No WebAuthn inside LINE's WebView. The in-LINE offline lock options are PIN and device-trust (on-pin); biometrics only become available after detaching to the browser or PWA shell. IndexedDB and crypto.subtleare available in all three shells — LIFF apps are HTTPS by requirement.

Scope

Login and openWindow only. No LIFF messaging or share APIs, no server code, no UI. The PWA-side helpers — persistence, the eviction guard, install prompts — live in in-pwa. The two packages share the AppShellContext union structurally, with no dependency between them.

See also