@linkforty/core 1.25.4 → 1.27.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 +46 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +19 -0
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/launchpad.d.ts +178 -0
- package/dist/lib/launchpad.d.ts.map +1 -0
- package/dist/lib/launchpad.js +262 -0
- package/dist/lib/launchpad.js.map +1 -0
- package/dist/lib/launchpad.test.d.ts +2 -0
- package/dist/lib/launchpad.test.d.ts.map +1 -0
- package/dist/lib/launchpad.test.js +220 -0
- package/dist/lib/launchpad.test.js.map +1 -0
- package/dist/routes/links.d.ts.map +1 -1
- package/dist/routes/links.js +10 -4
- package/dist/routes/links.js.map +1 -1
- package/dist/routes/redirect.d.ts +19 -0
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +165 -47
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/redirect.launchpad.test.d.ts +2 -0
- package/dist/routes/redirect.launchpad.test.d.ts.map +1 -0
- package/dist/routes/redirect.launchpad.test.js +398 -0
- package/dist/routes/redirect.launchpad.test.js.map +1 -0
- package/dist/routes/redirect.no-web-destination.test.js +32 -11
- package/dist/routes/redirect.no-web-destination.test.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/llms.txt +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -241,6 +241,32 @@ Automatically redirects users to the appropriate URL based on device type (iOS/A
|
|
|
241
241
|
|
|
242
242
|
**Mobile interstitial:** When a link has `appScheme` configured and a store fallback URL (iOS App Store or Google Play), mobile requests receive a smart interstitial page instead of a raw 302 redirect. The interstitial tries to open the app via URI scheme and falls back to the app store after 1.5 seconds. This handles the case where a 302 to a custom URI scheme fails silently when the app is not installed. URL fragments are preserved through the redirect, enabling patterns like E2E encryption where the decryption key lives in the fragment.
|
|
243
243
|
|
|
244
|
+
**Launchpad page (desktop):** A link to app-only content has nowhere to send a desktop visitor. When the chain resolves no web destination (link → template → workspace `appConfig`), a desktop request gets a hosted landing page instead of an error: the link's title, description and image (`ogTitle` / `ogDescription` / `ogImageUrl`, falling back to `title` / `description`), the app's icon and name, App Store / Google Play buttons, and a QR code so the visitor can finish on their phone. The page is `noindex`, sent with `Cache-Control: no-store` and a content-security policy that allows no script without a per-response nonce, and it never navigates on its own — no timers, no automatic scheme attempts.
|
|
245
|
+
|
|
246
|
+
When the link does have a web destination, the page offers it as a "Continue on the web" link, so a visitor is never trapped. By default it appears **only** where the alternative is nothing; a link that resolves to a destination still gets its 302. Two knobs change that:
|
|
247
|
+
|
|
248
|
+
- `organizations.settings.launchpad` (per workspace):
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"launchpad": {
|
|
253
|
+
"desktop": "no-destination", // "no-destination" (default) | "always" | "off"
|
|
254
|
+
"mobile": "store", // "store" (default) | "page"
|
|
255
|
+
"appName": "Ride Alert",
|
|
256
|
+
"appIconUrl": "https://cdn.example/icon.png",
|
|
257
|
+
"accentColor": "#0f766e"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `links.launchpad_mode` (per link, `launchpadMode` on the links API): `inherit` (default), `on` or `off`. The link's value wins over the workspace's.
|
|
263
|
+
|
|
264
|
+
With `desktop: "off"` (or `launchpad_mode: "off"`), a link with no web destination gets the plain "This link opens in an app" page instead.
|
|
265
|
+
|
|
266
|
+
**Launchpad page (mobile):** With `mobile: "page"`, a phone visitor whose platform has no Universal Link / App Link on the link gets the same page instead of the store redirect or the scheme interstitial: an "Open in app" button when the link has a URI scheme (a tap, not a navigation — the URL fragment is appended on tap), the visitor's own store button, and the web fallback as a "Continue on the web" link. That last one matters inside in-app browsers (Gmail, Facebook, Instagram…), which bypass Universal Links: the store redirect would send someone who *has* the app to the store, so the redirect path prefers the web fallback there to give the OS a second chance on the next hop — and the page keeps that hop available as a tap. Links that carry a Universal Link / App Link keep their 302 regardless, because the OS resolves the installed case before the request reaches this server. `launchpad_mode: "off"` opts a link out; `"on"` does not force the page onto a workspace that chose `"store"`. The default, `"store"`, is unchanged behaviour.
|
|
267
|
+
|
|
268
|
+
Two hooks on `RedirectRouteOptions` let a host application extend the page without forking it — see [Server Options](#server-options).
|
|
269
|
+
|
|
244
270
|
### QR Codes
|
|
245
271
|
|
|
246
272
|
```bash
|
|
@@ -322,9 +348,28 @@ interface ServerOptions {
|
|
|
322
348
|
};
|
|
323
349
|
logger?: boolean; // Enable Fastify logger (default: true)
|
|
324
350
|
trustProxy?: boolean | number; // Trust X-Forwarded-For when behind a proxy (default: false)
|
|
351
|
+
redirect?: RedirectRouteOptions; // See below
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface RedirectRouteOptions {
|
|
355
|
+
abuseReportUrl?: string; // Linked from the link-safety warning page
|
|
356
|
+
launchpad?: {
|
|
357
|
+
// Supply richer content for the launchpad page than the link row carries:
|
|
358
|
+
// a rendered share image, a templated hero. Return null for the default.
|
|
359
|
+
// A hook that throws is logged and treated as null — the page never fails
|
|
360
|
+
// because of it. `heroHtml` is inserted unescaped: render it yourself,
|
|
361
|
+
// never from end-user input.
|
|
362
|
+
resolveContent?: (link, settings) => Promise<LaunchpadContent | null>;
|
|
363
|
+
// Endpoint that receives `{ linkId, event }` beacons from the page —
|
|
364
|
+
// "view" on load, "cta_ios" / "cta_android" / "cta_open" on tap. No
|
|
365
|
+
// cookies or identifiers. Unset means the page sends nothing.
|
|
366
|
+
beaconUrl?: string;
|
|
367
|
+
};
|
|
325
368
|
}
|
|
326
369
|
```
|
|
327
370
|
|
|
371
|
+
`renderLaunchpadPage()`, `defaultLaunchpadContent()` and `shouldServeLaunchpadOnDesktop()` are exported so a host can render the same page elsewhere (a settings preview, for instance).
|
|
372
|
+
|
|
328
373
|
### Running behind a reverse proxy
|
|
329
374
|
|
|
330
375
|
When Core runs behind a reverse proxy, CDN, or load balancer, set `trustProxy` so the server uses the real client IP from `X-Forwarded-For` for redirect targeting, geo, attribution, and fingerprinting. Pass it when creating the server (e.g. `trustProxy: true` or a number of proxy hops) or set the `TRUST_PROXY` environment variable (e.g. `TRUST_PROXY=1`). Client-provided `ipAddress` in the SDK install request body is **not** used as the trusted IP; it is optional debug metadata only and must not be relied on for attribution.
|
|
@@ -378,6 +423,7 @@ Core does not create a `users` table. Authentication and user management are the
|
|
|
378
423
|
| og_description | TEXT | Open Graph description |
|
|
379
424
|
| og_image_url | TEXT | Open Graph image URL |
|
|
380
425
|
| og_type | VARCHAR(50) | Open Graph type (default: "website") |
|
|
426
|
+
| launchpad_mode | VARCHAR(10) | Launchpad page: inherit / on / off |
|
|
381
427
|
| attribution_window_hours| INTEGER | Install attribution window (default: 168)|
|
|
382
428
|
| is_active | BOOLEAN | Active status |
|
|
383
429
|
| expires_at | TIMESTAMP | Expiration date |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FastifyInstance } from 'fastify';
|
|
2
2
|
import { DatabaseOptions } from './lib/database.js';
|
|
3
|
+
import { type RedirectRouteOptions } from './routes/redirect.js';
|
|
3
4
|
/**
|
|
4
5
|
* Configuration options for creating a LinkForty server instance.
|
|
5
6
|
*/
|
|
@@ -14,6 +15,8 @@ export interface ServerOptions {
|
|
|
14
15
|
logger?: boolean;
|
|
15
16
|
/** When true or a number (proxy hop count), Fastify trusts X-Forwarded-For so request.ip is the real client IP. Set when behind a reverse proxy. */
|
|
16
17
|
trustProxy?: boolean | number;
|
|
18
|
+
/** Options for the redirect routes: abuse-report link, launchpad page hooks. */
|
|
19
|
+
redirect?: RedirectRouteOptions;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Create and configure a LinkForty Fastify server instance.
|
|
@@ -31,7 +34,9 @@ export * from './lib/database.js';
|
|
|
31
34
|
export * from './lib/fingerprint.js';
|
|
32
35
|
export * from './lib/webhook.js';
|
|
33
36
|
export * from './lib/link-safety.js';
|
|
37
|
+
export * from './lib/launchpad.js';
|
|
34
38
|
export * from './lib/event-emitter.js';
|
|
35
39
|
export * from './types/index.js';
|
|
40
|
+
export type { RedirectRouteOptions } from './routes/redirect.js';
|
|
36
41
|
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
|
|
37
42
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAUjF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oJAAoJ;IACpJ,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTAiC7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function createServer(options = {}) {
|
|
|
40
40
|
// Routes
|
|
41
41
|
await fastify.register(healthRoutes);
|
|
42
42
|
await fastify.register(wellKnownRoutes);
|
|
43
|
-
await fastify.register(redirectRoutes);
|
|
43
|
+
await fastify.register(redirectRoutes, options.redirect ?? {});
|
|
44
44
|
await fastify.register(linkRoutes);
|
|
45
45
|
await fastify.register(analyticsRoutes);
|
|
46
46
|
await fastify.register(sdkRoutes);
|
|
@@ -56,6 +56,7 @@ export * from './lib/database.js';
|
|
|
56
56
|
export * from './lib/fingerprint.js';
|
|
57
57
|
export * from './lib/webhook.js';
|
|
58
58
|
export * from './lib/link-safety.js';
|
|
59
|
+
export * from './lib/launchpad.js';
|
|
59
60
|
export * from './lib/event-emitter.js';
|
|
60
61
|
export * from './types/index.js';
|
|
61
62
|
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,EAA6B,MAAM,sBAAsB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAoBlD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAyB,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;QAC5D,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CAAC,CAAC;IAEH,OAAO;IACP,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG;KACpC,CAAC,CAAC;IAEH,mBAAmB;IACnB,IAAI,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC5B,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;SACvB,CAAC,CAAC;IACL,CAAC;IAED,WAAW;IACX,MAAM,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE3C,SAAS;IACT,MAAM,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC/D,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gCAAgC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA4BvB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAG5C,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA4BvB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAG5C,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBAuoBrE"}
|
package/dist/lib/database.js
CHANGED
|
@@ -335,6 +335,25 @@ export async function initializeDatabase(options = {}) {
|
|
|
335
335
|
ALTER TABLE links ADD COLUMN og_image_url TEXT;
|
|
336
336
|
END IF;
|
|
337
337
|
END $$;
|
|
338
|
+
`);
|
|
339
|
+
// Per-link override for the launchpad page (see lib/launchpad.ts):
|
|
340
|
+
// inherit — follow the workspace setting (the default)
|
|
341
|
+
// on — serve the page to desktop visitors even when the link has a
|
|
342
|
+
// web destination
|
|
343
|
+
// off — never serve it; a link with no web destination gets the plain
|
|
344
|
+
// "opens in an app" page instead
|
|
345
|
+
// NOT NULL with a default, so a cached row from before the column existed
|
|
346
|
+
// and a freshly inserted row read the same way.
|
|
347
|
+
await client.query(`
|
|
348
|
+
DO $$
|
|
349
|
+
BEGIN
|
|
350
|
+
IF NOT EXISTS (
|
|
351
|
+
SELECT 1 FROM information_schema.columns
|
|
352
|
+
WHERE table_name='links' AND column_name='launchpad_mode'
|
|
353
|
+
) THEN
|
|
354
|
+
ALTER TABLE links ADD COLUMN launchpad_mode VARCHAR(10) NOT NULL DEFAULT 'inherit';
|
|
355
|
+
END IF;
|
|
356
|
+
END $$;
|
|
338
357
|
`);
|
|
339
358
|
await client.query(`
|
|
340
359
|
DO $$
|
package/dist/lib/database.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAUpB,MAAM,CAAC,IAAI,EAAW,CAAC;AAEvB,+CAA+C;AAC/C,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,qDAAqD;AACrD,KAAK,UAAU,gBAAgB,CAAC,aAAqB,EAAE,EAAE,YAAoB,IAAI;IAC/E,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;YAC5D,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;gBAC1D,MAAM,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,sBAAsB;gBAC1E,OAAO,CAAC,GAAG,CAAC,+BAA+B,OAAO,wBAAwB,KAAK,OAAO,CAAC,CAAC;gBACxF,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,kDAAkD,EAAE,KAAK,CAAC,CAAC;gBACzE,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAE5C,6BAA6B;AAC7B,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAA2B,EAAE;IACpE,kBAAkB;IAClB,EAAE,GAAG,IAAI,IAAI,CAAC;QACZ,gBAAgB,EAAE,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,yDAAyD;QACtH,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK;QAClF,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC;QAC3B,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE;KAC7B,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAExC,oDAAoD;IACpD,EAAE;IACF,0EAA0E;IAC1E,0EAA0E;IAC1E,yDAAyD;IACzD,+EAA+E;IAC/E,6DAA6D;IAC7D,yEAAyE;IACzE,SAAS;IACT,EAAE;IACF,2EAA2E;IAC3E,uEAAuE;IACvE,wEAAwE;IACxE,8EAA8E;IAC9E,qDAAqD;IACrD,EAAE;IACF,2EAA2E;IAC3E,2EAA2E;IAC3E,6EAA6E;IAC7E,uDAAuD;IACvD,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;QAC1E,QAAQ,GAAG,IAAI,CAAC;QAEhB,2EAA2E;QAC3E,EAAE;QACF,8EAA8E;QAC9E,8EAA8E;QAC9E,8EAA8E;QAC9E,4EAA4E;QAC5E,yDAAyD;QACzD,EAAE;QACF,0EAA0E;QAC1E,+EAA+E;QAC/E,8EAA8E;QAC9E,8BAA8B;QAC9B,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;KASlB,CAAC,CAAC;QAEH,2EAA2E;QAC3E,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;KAYlB,CAAC,CAAC;QAEH,cAAc;QACd,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;KAmBlB,CAAC,CAAC;QAEH,qBAAqB;QACrB,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;KAwBlB,CAAC,CAAC;QAEH,kGAAkG;QAClG,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;KAelB,CAAC,CAAC;QAEH,2FAA2F;QAC3F,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;KAyBlB,CAAC,CAAC;QAEH,kEAAkE;QAClE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;KASlB,CAAC,CAAC;QAEH,qEAAqE;QACrE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;KAelB,CAAC,CAAC;QAEH,6CAA6C;QAC7C,EAAE;QACF,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,iEAAiE;QACjE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,wCAAwC;QACxC,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,sBAAsB;QACtB,EAAE;QACF,6EAA6E;QAC7E,yCAAyC;QACzC,EAAE;QACF,gFAAgF;QAChF,8EAA8E;QAC9E,8EAA8E;QAC9E,6EAA6E;QAC7E,gEAAgE;QAChE,iEAAiE;QACjE,+EAA+E;QAC/E,EAAE;QACF,2EAA2E;QAC3E,6BAA6B;QAC7B,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;KAsBlB,CAAC,CAAC;QAEH,qEAAqE;QACrE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,4DAA4D;QAC5D,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,mEAAmE;QACnE,yDAAyD;QACzD,0EAA0E;QAC1E,8BAA8B;QAC9B,4EAA4E;QAC5E,6CAA6C;QAC7C,0EAA0E;QAC1E,gDAAgD;QAChD,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,6EAA6E;QAC7E,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,kDAAkD;QAClD,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;KAalB,CAAC,CAAC;QAEH,0DAA0D;QAC1D,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;KAalB,CAAC,CAAC;QAEH,kFAAkF;QAClF,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,oCAAoC;QACpC,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,kCAAkC;QAClC,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,kDAAkD;QAClD,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,4DAA4D;QAC5D,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,2EAA2E;QAC3E,4EAA4E;QAC5E,yEAAyE;QACzE,4EAA4E;QAC5E,wEAAwE;QACxE,sEAAsE;QACtE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,4DAA4D;QAC5D,6EAA6E;QAC7E,4EAA4E;QAC5E,2EAA2E;QAC3E,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,iEAAiE;QACjE,wEAAwE;QACxE,0EAA0E;QAC1E,2CAA2C;QAC3C,EAAE;QACF,4EAA4E;QAC5E,sEAAsE;QACtE,4EAA4E;QAC5E,oDAAoD;QACpD,EAAE;QACF,wEAAwE;QACxE,0EAA0E;QAC1E,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;KAOlB,CAAC,CAAC;QAEH,8DAA8D;QAC9D,6EAA6E;QAC7E,8EAA8E;QAC9E,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,mDAAmD;QACnD,6EAA6E;QAC7E,4EAA4E;QAC5E,6EAA6E;QAC7E,wEAAwE;QACxE,+EAA+E;QAC/E,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;KAgBlB,CAAC,CAAC;QAEH,iEAAiE;QACjE,wEAAwE;QACxE,4EAA4E;QAC5E,4EAA4E;QAC5E,8EAA8E;QAC9E,2EAA2E;QAC3E,yEAAyE;QACzE,2EAA2E;QAC3E,oBAAoB;QACpB,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;KAgBlB,CAAC,CAAC;QAEH,iCAAiC;QACjC,MAAM,MAAM,CAAC,KAAK,CAAC,6EAA6E,CAAC,CAAC;QAClG,MAAM,MAAM,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;QACrF,MAAM,MAAM,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;QAChG,MAAM,MAAM,CAAC,KAAK,CAAC,wEAAwE,CAAC,CAAC;QAC7F,MAAM,MAAM,CAAC,KAAK,CAAC,kFAAkF,CAAC,CAAC;QACvG,MAAM,MAAM,CAAC,KAAK,CAAC,2FAA2F,CAAC,CAAC;QAChH,qFAAqF;QACrF,MAAM,MAAM,CAAC,KAAK,CAAC,sHAAsH,CAAC,CAAC;QAC3I,oCAAoC;QACpC,MAAM,MAAM,CAAC,KAAK,CAAC,2FAA2F,CAAC,CAAC;QAChH,MAAM,MAAM,CAAC,KAAK,CAAC,uFAAuF,CAAC,CAAC;QAC5G,MAAM,MAAM,CAAC,KAAK,CAAC,yFAAyF,CAAC,CAAC;QAC9G,MAAM,MAAM,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;QACjG,MAAM,MAAM,CAAC,KAAK,CAAC,wFAAwF,CAAC,CAAC;QAC7G,MAAM,MAAM,CAAC,KAAK,CAAC,iGAAiG,CAAC,CAAC;QAEtH,6BAA6B;QAC7B,MAAM,MAAM,CAAC,KAAK,CAAC,mFAAmF,CAAC,CAAC;QACxG,MAAM,MAAM,CAAC,KAAK,CAAC,kFAAkF,CAAC,CAAC;QACvG,MAAM,MAAM,CAAC,KAAK,CAAC,wEAAwE,CAAC,CAAC;QAC7F,MAAM,MAAM,CAAC,KAAK,CAAC,gFAAgF,CAAC,CAAC;QAErG,uBAAuB;QACvB,MAAM,MAAM,CAAC,KAAK,CAAC,sEAAsE,CAAC,CAAC;QAC3F,MAAM,MAAM,CAAC,KAAK,CAAC,8FAA8F,CAAC,CAAC;QAEnH,4BAA4B;QAC5B,MAAM,MAAM,CAAC,KAAK,CAAC,sFAAsF,CAAC,CAAC;QAC3G,MAAM,MAAM,CAAC,KAAK,CAAC,gFAAgF,CAAC,CAAC;QACrG,MAAM,MAAM,CAAC,KAAK,CAAC,+FAA+F,CAAC,CAAC;QACpH,iFAAiF;QACjF,MAAM,MAAM,CAAC,KAAK,CAAC,yHAAyH,CAAC,CAAC;QAC9I,+EAA+E;QAC/E,0EAA0E;QAC1E,MAAM,MAAM,CAAC,KAAK,CAAC,gHAAgH,CAAC,CAAC;QAErI,kEAAkE;QAClE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;KAUlB,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;IAC1D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QACrD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,QAAQ,EAAE,CAAC;YACb,qEAAqE;YACrE,8DAA8D;YAC9D,MAAM,MAAM;iBACT,KAAK,CAAC,+BAA+B,EAAE,CAAC,oBAAoB,CAAC,CAAC;iBAC9D,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5B,CAAC;QACD,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launchpad: the page a visitor sees when a link cannot open the app.
|
|
3
|
+
*
|
|
4
|
+
* A short link to app-only content has nowhere good to send a desktop visitor,
|
|
5
|
+
* and a phone without the app installed otherwise gets a bare store redirect.
|
|
6
|
+
* This page is the alternative: the link's title, description and image, the
|
|
7
|
+
* app's icon and name, store buttons, a QR code so a desktop visitor can finish
|
|
8
|
+
* on their phone, and — when the app has a URI scheme — an "Open in app" button.
|
|
9
|
+
*
|
|
10
|
+
* Three rules shape everything below:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Never add a hop to a link that already works.** The page is served by
|
|
13
|
+
* default only when a link has no web destination at any level. Serving it
|
|
14
|
+
* in front of a working destination is a per-workspace or per-link choice.
|
|
15
|
+
* 2. **Never navigate on the visitor's behalf.** No timers, no automatic scheme
|
|
16
|
+
* attempts. A forced `myapp://` open with the app absent produces an OS error
|
|
17
|
+
* alert and a dead tab; a button the visitor taps produces neither.
|
|
18
|
+
* 3. **Nothing on the page is authored by the visitor's target.** Every value
|
|
19
|
+
* is escaped; the one exception, `heroHtml`, is rendered by the host
|
|
20
|
+
* application from its own templates and is documented as such below.
|
|
21
|
+
*
|
|
22
|
+
* The page is search-engine invisible (`noindex`, and the route sends
|
|
23
|
+
* `X-Robots-Tag` and `Cache-Control: no-store`), because a redirect host that
|
|
24
|
+
* indexes one page per short link is a thin-content spam surface.
|
|
25
|
+
*/
|
|
26
|
+
export type LaunchpadDesktopMode = 'no-destination' | 'always' | 'off';
|
|
27
|
+
export type LaunchpadMobileMode = 'store' | 'page';
|
|
28
|
+
export type LaunchpadLinkMode = 'inherit' | 'on' | 'off';
|
|
29
|
+
/**
|
|
30
|
+
* Per-workspace configuration, read from `organizations.settings.launchpad`.
|
|
31
|
+
* Every field is optional; an absent object means "the defaults", which are
|
|
32
|
+
* today's behaviour everywhere except the no-destination desktop case.
|
|
33
|
+
*/
|
|
34
|
+
export interface LaunchpadSettings {
|
|
35
|
+
/** When a desktop visitor gets the page. Default `no-destination`. */
|
|
36
|
+
desktop?: LaunchpadDesktopMode;
|
|
37
|
+
/**
|
|
38
|
+
* What a mobile visitor gets when the link has no Universal Link / App Link
|
|
39
|
+
* for their platform. Default `store`: today's behaviour — a redirect to the
|
|
40
|
+
* store, or the scheme interstitial when the link has a URI scheme. `page`
|
|
41
|
+
* serves the launchpad page instead, with "Open in app" as a button the
|
|
42
|
+
* visitor taps rather than a navigation the page performs.
|
|
43
|
+
*/
|
|
44
|
+
mobile?: LaunchpadMobileMode;
|
|
45
|
+
/** Shown in the page header, and as the `<title>` fallback. */
|
|
46
|
+
appName?: string;
|
|
47
|
+
/** Absolute https URL of the app icon, shown at 64×64. */
|
|
48
|
+
appIconUrl?: string;
|
|
49
|
+
/** `#rrggbb`. Drives the primary button. Anything else is ignored. */
|
|
50
|
+
accentColor?: string;
|
|
51
|
+
/** Opaque to Core: a host application may key its own page templates on it. */
|
|
52
|
+
templateId?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface LaunchpadTheme {
|
|
55
|
+
appName?: string;
|
|
56
|
+
appIconUrl?: string;
|
|
57
|
+
accentColor?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What the page shows. Produced by `defaultLaunchpadContent()` from the link
|
|
61
|
+
* row, or by a host application's `resolveContent` hook when it has richer
|
|
62
|
+
* material (a rendered share card, a template).
|
|
63
|
+
*/
|
|
64
|
+
export interface LaunchpadContent {
|
|
65
|
+
title: string;
|
|
66
|
+
description: string;
|
|
67
|
+
/** Hero image, absolute http(s) URL. Also used for `og:image`. */
|
|
68
|
+
imageUrl: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Pre-rendered HTML for the hero region, inserted **without escaping**.
|
|
71
|
+
*
|
|
72
|
+
* Trust boundary: this must be produced by the host application from its own
|
|
73
|
+
* templates, never from visitor or end-user input. The page's CSP allows no
|
|
74
|
+
* script without the per-response nonce, which the host does not know, so a
|
|
75
|
+
* mistake here cannot execute — but it can still deface. Leave it unset unless
|
|
76
|
+
* you render it yourself.
|
|
77
|
+
*/
|
|
78
|
+
heroHtml?: string;
|
|
79
|
+
theme: LaunchpadTheme;
|
|
80
|
+
}
|
|
81
|
+
/** The subset of a `links` row the default content is built from. */
|
|
82
|
+
export interface LaunchpadLinkFields {
|
|
83
|
+
title?: string | null;
|
|
84
|
+
description?: string | null;
|
|
85
|
+
og_title?: string | null;
|
|
86
|
+
og_description?: string | null;
|
|
87
|
+
og_image_url?: string | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read `settings.launchpad` off a workspace settings JSON blob, tolerating any
|
|
91
|
+
* shape. Settings are written by whatever administers the workspace, not by
|
|
92
|
+
* this package, so nothing here is trusted: unknown modes fall back to the
|
|
93
|
+
* default, a bad colour is dropped, and image URLs must be http(s).
|
|
94
|
+
*/
|
|
95
|
+
export declare function readLaunchpadSettings(orgSettings: unknown): LaunchpadSettings;
|
|
96
|
+
/** Normalise `links.launchpad_mode`, which a cached row from before the column existed may lack. */
|
|
97
|
+
export declare function readLaunchpadLinkMode(value: unknown): LaunchpadLinkMode;
|
|
98
|
+
/**
|
|
99
|
+
* Whether a desktop visitor gets the page.
|
|
100
|
+
*
|
|
101
|
+
* The link's own mode wins outright. Otherwise the workspace decides, and the
|
|
102
|
+
* default serves the page only where the alternative is nothing at all.
|
|
103
|
+
*/
|
|
104
|
+
export declare function shouldServeLaunchpadOnDesktop(input: {
|
|
105
|
+
hasWebDestination: boolean;
|
|
106
|
+
orgMode?: LaunchpadDesktopMode;
|
|
107
|
+
linkMode?: LaunchpadLinkMode;
|
|
108
|
+
}): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a mobile visitor gets the page.
|
|
111
|
+
*
|
|
112
|
+
* Only when the workspace chose `page`, and never for a platform whose link
|
|
113
|
+
* carries a Universal Link / App Link: the OS resolves the installed case
|
|
114
|
+
* before this server sees the click, and the 302 to that URL must stay so it
|
|
115
|
+
* can. A link's `on` does not force the page onto a workspace that chose
|
|
116
|
+
* `store` — that would be the extra hop other products had to add a skip flag
|
|
117
|
+
* for — but `off` still opts a link out.
|
|
118
|
+
*/
|
|
119
|
+
export declare function shouldServeLaunchpadOnMobile(input: {
|
|
120
|
+
mobileMode?: LaunchpadMobileMode;
|
|
121
|
+
linkMode?: LaunchpadLinkMode;
|
|
122
|
+
hasAppOpenPath: boolean;
|
|
123
|
+
}): boolean;
|
|
124
|
+
/** Content from the link row alone: what any deployment can show with no extra machinery. */
|
|
125
|
+
export declare function defaultLaunchpadContent(link: LaunchpadLinkFields, settings: LaunchpadSettings): LaunchpadContent;
|
|
126
|
+
/** A fresh CSP nonce per response. */
|
|
127
|
+
export declare function createLaunchpadNonce(): string;
|
|
128
|
+
/**
|
|
129
|
+
* The page's content-security policy.
|
|
130
|
+
*
|
|
131
|
+
* Scripts run only with this response's nonce. Inline `style` attributes are
|
|
132
|
+
* allowed because a host-rendered hero region carries them, and a nonce on
|
|
133
|
+
* `style-src` would make browsers ignore `'unsafe-inline'` — so the nonce
|
|
134
|
+
* guards scripts alone. `connect-src` admits the beacon endpoint's origin when
|
|
135
|
+
* one is configured; `sendBeacon` is governed by it.
|
|
136
|
+
*/
|
|
137
|
+
export declare function launchpadContentSecurityPolicy(nonce: string, beaconUrl?: string): string;
|
|
138
|
+
export interface LaunchpadPageContext {
|
|
139
|
+
content: LaunchpadContent;
|
|
140
|
+
/** The link's id; the QR code is fetched from `/api/links/{id}/qr`. */
|
|
141
|
+
linkId: string;
|
|
142
|
+
/** Absolute URL of this page, for `og:url`. */
|
|
143
|
+
pageUrl: string;
|
|
144
|
+
iosUrl: string | null;
|
|
145
|
+
androidUrl: string | null;
|
|
146
|
+
/**
|
|
147
|
+
* The link's web destination, when it has one. Rendered as a plain
|
|
148
|
+
* "Continue on the web" link so the page is never a dead end: a desktop
|
|
149
|
+
* visitor in `always` mode still has the destination the 302 would have
|
|
150
|
+
* given them, and a phone visitor inside an in-app browser keeps the path
|
|
151
|
+
* that lets a Universal Link fire on the next hop.
|
|
152
|
+
*/
|
|
153
|
+
webUrl?: string | null;
|
|
154
|
+
/**
|
|
155
|
+
* The app's URI-scheme URL for this link, or null. When set, an "Open in app"
|
|
156
|
+
* button is rendered whose only job is to navigate to it on tap. The URL
|
|
157
|
+
* fragment of the page is appended on tap, so a key that lives only in the
|
|
158
|
+
* fragment survives without ever reaching the server.
|
|
159
|
+
*/
|
|
160
|
+
schemeUrl: string | null;
|
|
161
|
+
/** Show the QR block. Meant for desktop; a phone scanning itself is no use. */
|
|
162
|
+
showQr: boolean;
|
|
163
|
+
nonce: string;
|
|
164
|
+
/**
|
|
165
|
+
* When set, the page reports `view` on load and `cta_ios` / `cta_android` /
|
|
166
|
+
* `cta_open` / `cta_web` on the matching tap via `navigator.sendBeacon`, as a JSON body
|
|
167
|
+
* `{ linkId, event }`. No cookies, no identifiers. Absent by default; the
|
|
168
|
+
* endpoint is the host application's to provide.
|
|
169
|
+
*/
|
|
170
|
+
beaconUrl?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Render the full HTML document. Pure: same context, same output (the nonce
|
|
174
|
+
* is an input). Every string from `content`, the URLs and the theme is
|
|
175
|
+
* escaped; `content.heroHtml` is the documented exception.
|
|
176
|
+
*/
|
|
177
|
+
export declare function renderLaunchpadPage(ctx: LaunchpadPageContext): string;
|
|
178
|
+
//# sourceMappingURL=launchpad.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchpad.d.ts","sourceRoot":"","sources":["../../src/lib/launchpad.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { escapeHtml, safeHref } from './link-safety.js';
|
|
3
|
+
const DESKTOP_MODES = ['no-destination', 'always', 'off'];
|
|
4
|
+
const MOBILE_MODES = ['store', 'page'];
|
|
5
|
+
const LINK_MODES = ['inherit', 'on', 'off'];
|
|
6
|
+
const HEX_COLOR = /^#[0-9a-f]{6}$/i;
|
|
7
|
+
const asString = (v, max) => typeof v === 'string' && v.trim() ? v.trim().slice(0, max) : undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Read `settings.launchpad` off a workspace settings JSON blob, tolerating any
|
|
10
|
+
* shape. Settings are written by whatever administers the workspace, not by
|
|
11
|
+
* this package, so nothing here is trusted: unknown modes fall back to the
|
|
12
|
+
* default, a bad colour is dropped, and image URLs must be http(s).
|
|
13
|
+
*/
|
|
14
|
+
export function readLaunchpadSettings(orgSettings) {
|
|
15
|
+
const raw = orgSettings && typeof orgSettings === 'object' && orgSettings.launchpad;
|
|
16
|
+
if (!raw || typeof raw !== 'object')
|
|
17
|
+
return {};
|
|
18
|
+
const r = raw;
|
|
19
|
+
const desktop = DESKTOP_MODES.find((m) => m === r.desktop);
|
|
20
|
+
const mobile = MOBILE_MODES.find((m) => m === r.mobile);
|
|
21
|
+
const accentColor = asString(r.accentColor, 7);
|
|
22
|
+
const appIconUrl = asString(r.appIconUrl, 2048);
|
|
23
|
+
return {
|
|
24
|
+
...(desktop ? { desktop } : {}),
|
|
25
|
+
...(mobile ? { mobile } : {}),
|
|
26
|
+
...(asString(r.appName, 60) ? { appName: asString(r.appName, 60) } : {}),
|
|
27
|
+
...(appIconUrl && safeHref(appIconUrl) ? { appIconUrl } : {}),
|
|
28
|
+
...(accentColor && HEX_COLOR.test(accentColor) ? { accentColor } : {}),
|
|
29
|
+
...(asString(r.templateId, 64) ? { templateId: asString(r.templateId, 64) } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Normalise `links.launchpad_mode`, which a cached row from before the column existed may lack. */
|
|
33
|
+
export function readLaunchpadLinkMode(value) {
|
|
34
|
+
return LINK_MODES.find((m) => m === value) ?? 'inherit';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether a desktop visitor gets the page.
|
|
38
|
+
*
|
|
39
|
+
* The link's own mode wins outright. Otherwise the workspace decides, and the
|
|
40
|
+
* default serves the page only where the alternative is nothing at all.
|
|
41
|
+
*/
|
|
42
|
+
export function shouldServeLaunchpadOnDesktop(input) {
|
|
43
|
+
const linkMode = input.linkMode ?? 'inherit';
|
|
44
|
+
if (linkMode === 'off')
|
|
45
|
+
return false;
|
|
46
|
+
if (linkMode === 'on')
|
|
47
|
+
return true;
|
|
48
|
+
switch (input.orgMode ?? 'no-destination') {
|
|
49
|
+
case 'always':
|
|
50
|
+
return true;
|
|
51
|
+
case 'off':
|
|
52
|
+
return false;
|
|
53
|
+
default:
|
|
54
|
+
return !input.hasWebDestination;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether a mobile visitor gets the page.
|
|
59
|
+
*
|
|
60
|
+
* Only when the workspace chose `page`, and never for a platform whose link
|
|
61
|
+
* carries a Universal Link / App Link: the OS resolves the installed case
|
|
62
|
+
* before this server sees the click, and the 302 to that URL must stay so it
|
|
63
|
+
* can. A link's `on` does not force the page onto a workspace that chose
|
|
64
|
+
* `store` — that would be the extra hop other products had to add a skip flag
|
|
65
|
+
* for — but `off` still opts a link out.
|
|
66
|
+
*/
|
|
67
|
+
export function shouldServeLaunchpadOnMobile(input) {
|
|
68
|
+
if ((input.linkMode ?? 'inherit') === 'off')
|
|
69
|
+
return false;
|
|
70
|
+
if ((input.mobileMode ?? 'store') !== 'page')
|
|
71
|
+
return false;
|
|
72
|
+
return !input.hasAppOpenPath;
|
|
73
|
+
}
|
|
74
|
+
/** Content from the link row alone: what any deployment can show with no extra machinery. */
|
|
75
|
+
export function defaultLaunchpadContent(link, settings) {
|
|
76
|
+
const image = link.og_image_url && safeHref(link.og_image_url) ? link.og_image_url : null;
|
|
77
|
+
return {
|
|
78
|
+
title: link.og_title || link.title || settings.appName || 'Open in the app',
|
|
79
|
+
description: link.og_description || link.description || '',
|
|
80
|
+
imageUrl: image,
|
|
81
|
+
theme: {
|
|
82
|
+
...(settings.appName ? { appName: settings.appName } : {}),
|
|
83
|
+
...(settings.appIconUrl ? { appIconUrl: settings.appIconUrl } : {}),
|
|
84
|
+
...(settings.accentColor ? { accentColor: settings.accentColor } : {}),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** WCAG relative luminance above ~0.5 reads better with dark text on it. */
|
|
89
|
+
function isLightColor(hex) {
|
|
90
|
+
const channel = (i) => {
|
|
91
|
+
const c = parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
92
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
93
|
+
};
|
|
94
|
+
return 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5) > 0.5;
|
|
95
|
+
}
|
|
96
|
+
/** A fresh CSP nonce per response. */
|
|
97
|
+
export function createLaunchpadNonce() {
|
|
98
|
+
return randomBytes(16).toString('base64');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The page's content-security policy.
|
|
102
|
+
*
|
|
103
|
+
* Scripts run only with this response's nonce. Inline `style` attributes are
|
|
104
|
+
* allowed because a host-rendered hero region carries them, and a nonce on
|
|
105
|
+
* `style-src` would make browsers ignore `'unsafe-inline'` — so the nonce
|
|
106
|
+
* guards scripts alone. `connect-src` admits the beacon endpoint's origin when
|
|
107
|
+
* one is configured; `sendBeacon` is governed by it.
|
|
108
|
+
*/
|
|
109
|
+
export function launchpadContentSecurityPolicy(nonce, beaconUrl) {
|
|
110
|
+
const connect = new Set(["'self'"]);
|
|
111
|
+
if (beaconUrl) {
|
|
112
|
+
try {
|
|
113
|
+
connect.add(new URL(beaconUrl).origin);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Relative beacon URL — 'self' covers it.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [
|
|
120
|
+
"default-src 'none'",
|
|
121
|
+
"img-src 'self' https: data:",
|
|
122
|
+
"style-src 'unsafe-inline'",
|
|
123
|
+
`script-src 'nonce-${nonce}'`,
|
|
124
|
+
`connect-src ${[...connect].join(' ')}`,
|
|
125
|
+
"base-uri 'none'",
|
|
126
|
+
"form-action 'none'",
|
|
127
|
+
].join('; ');
|
|
128
|
+
}
|
|
129
|
+
const STYLES = `
|
|
130
|
+
:root { --lp-ink: #16181d; --lp-muted: #5f6673; --lp-bg: #ffffff; --lp-surface: #f4f5f7; --lp-line: #e2e5ea; --lp-accent: #2563eb; --lp-accent-ink: #ffffff; }
|
|
131
|
+
@media (prefers-color-scheme: dark) { :root { --lp-ink: #eef0f3; --lp-muted: #a3abb8; --lp-bg: #121418; --lp-surface: #1c1f25; --lp-line: #2b3038; } }
|
|
132
|
+
* { box-sizing: border-box; }
|
|
133
|
+
body { margin: 0; background: var(--lp-bg); color: var(--lp-ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; }
|
|
134
|
+
.lp { max-width: 40rem; margin: 0 auto; padding: 2rem 1.25rem 3rem; display: flex; flex-direction: column; gap: 1.5rem; }
|
|
135
|
+
.lp-app { display: flex; align-items: center; gap: 0.75rem; min-height: 1rem; }
|
|
136
|
+
.lp-app img { width: 48px; height: 48px; border-radius: 12px; object-fit: cover; background: var(--lp-surface); }
|
|
137
|
+
.lp-app span { font-weight: 600; font-size: 1rem; }
|
|
138
|
+
.lp-hero img { display: block; width: 100%; max-height: 22rem; object-fit: cover; border-radius: 14px; background: var(--lp-surface); margin-bottom: 1rem; }
|
|
139
|
+
.lp-hero h1 { font-size: 1.75rem; line-height: 1.2; margin: 0 0 0.5rem; text-wrap: balance; }
|
|
140
|
+
.lp-hero p { margin: 0; color: var(--lp-muted); font-size: 1.05rem; }
|
|
141
|
+
.lp-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; }
|
|
142
|
+
.lp-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 0.8rem 1.25rem; border-radius: 10px; font-size: 0.95rem; font-weight: 600; text-decoration: none; border: 1px solid var(--lp-line); background: var(--lp-surface); color: var(--lp-ink); }
|
|
143
|
+
.lp-btn-primary { background: var(--lp-accent); border-color: var(--lp-accent); color: var(--lp-accent-ink); }
|
|
144
|
+
.lp-btn:focus-visible { outline: 2px solid var(--lp-accent); outline-offset: 2px; }
|
|
145
|
+
.lp-web { margin: -0.5rem 0 0; font-size: 0.95rem; }
|
|
146
|
+
.lp-web a { color: var(--lp-muted); text-decoration: underline; text-underline-offset: 3px; }
|
|
147
|
+
.lp-web a:hover { color: var(--lp-ink); }
|
|
148
|
+
.lp-qr { display: flex; align-items: center; gap: 1.25rem; padding: 1rem; border: 1px solid var(--lp-line); border-radius: 14px; background: var(--lp-surface); }
|
|
149
|
+
.lp-qr img { width: 132px; height: 132px; border-radius: 8px; background: #fff; flex: none; }
|
|
150
|
+
.lp-qr strong { display: block; font-size: 1rem; margin-bottom: 0.2rem; }
|
|
151
|
+
.lp-qr p { margin: 0; color: var(--lp-muted); font-size: 0.9rem; }
|
|
152
|
+
@media (max-width: 480px) { .lp-qr { flex-direction: column; text-align: center; } .lp-hero h1 { font-size: 1.5rem; } }
|
|
153
|
+
`;
|
|
154
|
+
/** Only shipped when a button or a beacon needs it; a page with neither has no script at all. */
|
|
155
|
+
const SCRIPT = `
|
|
156
|
+
(function () {
|
|
157
|
+
var body = document.body;
|
|
158
|
+
var open = document.getElementById('lp-open');
|
|
159
|
+
if (open) {
|
|
160
|
+
open.addEventListener('click', function () {
|
|
161
|
+
open.href = open.getAttribute('data-scheme') + (window.location.hash || '');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
var beacon = body.getAttribute('data-beacon');
|
|
165
|
+
if (beacon && navigator.sendBeacon) {
|
|
166
|
+
var linkId = body.getAttribute('data-link-id');
|
|
167
|
+
var send = function (event) {
|
|
168
|
+
try { navigator.sendBeacon(beacon, JSON.stringify({ linkId: linkId, event: event })); } catch (e) {}
|
|
169
|
+
};
|
|
170
|
+
send('view');
|
|
171
|
+
var ctas = document.querySelectorAll('[data-lp-cta]');
|
|
172
|
+
for (var i = 0; i < ctas.length; i++) {
|
|
173
|
+
ctas[i].addEventListener('click', function (e) { send(e.currentTarget.getAttribute('data-lp-cta')); });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
`;
|
|
178
|
+
/**
|
|
179
|
+
* Render the full HTML document. Pure: same context, same output (the nonce
|
|
180
|
+
* is an input). Every string from `content`, the URLs and the theme is
|
|
181
|
+
* escaped; `content.heroHtml` is the documented exception.
|
|
182
|
+
*/
|
|
183
|
+
export function renderLaunchpadPage(ctx) {
|
|
184
|
+
const content = ctx.content;
|
|
185
|
+
const theme = content.theme;
|
|
186
|
+
const title = escapeHtml(content.title);
|
|
187
|
+
const description = escapeHtml(content.description);
|
|
188
|
+
const imageUrl = content.imageUrl && safeHref(content.imageUrl) ? escapeHtml(content.imageUrl) : null;
|
|
189
|
+
const iconUrl = theme.appIconUrl && safeHref(theme.appIconUrl) ? escapeHtml(theme.appIconUrl) : null;
|
|
190
|
+
const iosUrl = ctx.iosUrl && safeHref(ctx.iosUrl) ? escapeHtml(ctx.iosUrl) : null;
|
|
191
|
+
const androidUrl = ctx.androidUrl && safeHref(ctx.androidUrl) ? escapeHtml(ctx.androidUrl) : null;
|
|
192
|
+
const accent = theme.accentColor && HEX_COLOR.test(theme.accentColor) ? theme.accentColor : null;
|
|
193
|
+
const accentInk = accent && isLightColor(accent) ? '#16181d' : '#ffffff';
|
|
194
|
+
const nonce = escapeHtml(ctx.nonce);
|
|
195
|
+
const head = [
|
|
196
|
+
`<title>${title}</title>`,
|
|
197
|
+
description ? `<meta name="description" content="${description}">` : '',
|
|
198
|
+
`<meta property="og:type" content="website">`,
|
|
199
|
+
`<meta property="og:url" content="${escapeHtml(ctx.pageUrl)}">`,
|
|
200
|
+
`<meta property="og:title" content="${title}">`,
|
|
201
|
+
description ? `<meta property="og:description" content="${description}">` : '',
|
|
202
|
+
imageUrl ? `<meta property="og:image" content="${imageUrl}">` : '',
|
|
203
|
+
`<meta name="twitter:card" content="${imageUrl ? 'summary_large_image' : 'summary'}">`,
|
|
204
|
+
]
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.join('\n');
|
|
207
|
+
const header = iconUrl || theme.appName
|
|
208
|
+
? `<header class="lp-app">${iconUrl ? `<img src="${iconUrl}" alt="" width="48" height="48">` : ''}${theme.appName ? `<span>${escapeHtml(theme.appName)}</span>` : ''}</header>`
|
|
209
|
+
: '';
|
|
210
|
+
const hero = content.heroHtml
|
|
211
|
+
? `<section class="lp-hero">${content.heroHtml}</section>`
|
|
212
|
+
: `<section class="lp-hero">${imageUrl ? `<img src="${imageUrl}" alt="">` : ''}<h1>${title}</h1>${description ? `<p>${description}</p>` : ''}</section>`;
|
|
213
|
+
const buttons = [
|
|
214
|
+
ctx.schemeUrl
|
|
215
|
+
? `<a class="lp-btn lp-btn-primary" id="lp-open" data-lp-cta="cta_open" data-scheme="${escapeHtml(ctx.schemeUrl)}" href="${escapeHtml(ctx.schemeUrl)}">Open in app</a>`
|
|
216
|
+
: '',
|
|
217
|
+
iosUrl
|
|
218
|
+
? `<a class="lp-btn${ctx.schemeUrl ? '' : ' lp-btn-primary'}" data-lp-cta="cta_ios" href="${iosUrl}">Download on the App Store</a>`
|
|
219
|
+
: '',
|
|
220
|
+
androidUrl
|
|
221
|
+
? `<a class="lp-btn${ctx.schemeUrl || iosUrl ? '' : ' lp-btn-primary'}" data-lp-cta="cta_android" href="${androidUrl}">Get it on Google Play</a>`
|
|
222
|
+
: '',
|
|
223
|
+
]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join('');
|
|
226
|
+
const webUrl = ctx.webUrl && safeHref(ctx.webUrl) ? escapeHtml(ctx.webUrl) : null;
|
|
227
|
+
const actions = buttons ? `<section class="lp-actions">${buttons}</section>` : '';
|
|
228
|
+
const web = webUrl
|
|
229
|
+
? `<p class="lp-web"><a data-lp-cta="cta_web" href="${webUrl}">Continue on the web</a></p>`
|
|
230
|
+
: '';
|
|
231
|
+
const qr = ctx.showQr
|
|
232
|
+
? `<section class="lp-qr"><img src="/api/links/${escapeHtml(ctx.linkId)}/qr?format=svg&size=264" alt="QR code for this link" width="132" height="132"><div><strong>Scan to open on your phone</strong><p>Point your phone's camera at the code to open this link there.</p></div></section>`
|
|
233
|
+
: '';
|
|
234
|
+
const needsScript = Boolean(ctx.schemeUrl || ctx.beaconUrl);
|
|
235
|
+
const bodyAttrs = [
|
|
236
|
+
`data-link-id="${escapeHtml(ctx.linkId)}"`,
|
|
237
|
+
ctx.beaconUrl ? `data-beacon="${escapeHtml(ctx.beaconUrl)}"` : '',
|
|
238
|
+
]
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.join(' ');
|
|
241
|
+
return `<!DOCTYPE html>
|
|
242
|
+
<html lang="en">
|
|
243
|
+
<head>
|
|
244
|
+
<meta charset="utf-8">
|
|
245
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
246
|
+
<meta name="robots" content="noindex, nofollow">
|
|
247
|
+
${head}
|
|
248
|
+
<style>${STYLES}${accent ? `\n :root { --lp-accent: ${accent}; --lp-accent-ink: ${accentInk}; }` : ''}</style>
|
|
249
|
+
</head>
|
|
250
|
+
<body ${bodyAttrs}>
|
|
251
|
+
<main class="lp">
|
|
252
|
+
${header}
|
|
253
|
+
${hero}
|
|
254
|
+
${actions}
|
|
255
|
+
${web}
|
|
256
|
+
${qr}
|
|
257
|
+
</main>
|
|
258
|
+
${needsScript ? `<script nonce="${nonce}">${SCRIPT}</script>` : ''}
|
|
259
|
+
</body>
|
|
260
|
+
</html>`;
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=launchpad.js.map
|