liff
@noy-db/in-liff·pnpm add @noy-db/hub @noy-db/in-liffThe 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:
| Platform | What happens |
|---|---|
| Android | In-scope links open the installed PWA directly (WebAPK link capture), which shares Chrome's origin storage. |
| iOS | Always 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. |
| All | LINE'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. |
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
- in-* family
- pwa — the other half of the portal shell
- on-oidc — the split-key unlock this feeds
- share-link — the deep-link grammar
initLiffAppingests