@aglyn/shared-util-first-touch 1.0.0-beta.186
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/LICENSE +201 -0
- package/README.md +66 -0
- package/package.json +34 -0
- package/src/index.d.ts +20 -0
- package/src/index.js +21 -0
- package/src/index.js.map +1 -0
- package/src/lib/channel-rules.d.ts +96 -0
- package/src/lib/channel-rules.js +169 -0
- package/src/lib/channel-rules.js.map +1 -0
- package/src/lib/first-touch-page.d.ts +43 -0
- package/src/lib/first-touch-page.js +52 -0
- package/src/lib/first-touch-page.js.map +1 -0
- package/src/lib/first-touch-script.d.ts +39 -0
- package/src/lib/first-touch-script.js +52 -0
- package/src/lib/first-touch-script.js.map +1 -0
- package/src/lib/first-touch.d.ts +212 -0
- package/src/lib/first-touch.js +677 -0
- package/src/lib/first-touch.js.map +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { FirstTouchRuntime } from './first-touch';
|
|
18
|
+
/**
|
|
19
|
+
* The bridge between a SERVED capture script and the page's own code.
|
|
20
|
+
*
|
|
21
|
+
* A surface that includes the capture as a script tag has no import to call
|
|
22
|
+
* it through, and its consent code — which decides whether the record may be
|
|
23
|
+
* written to the device — runs in a bundle that never loaded the capture. So
|
|
24
|
+
* the served script leaves its runtime on `window`, and this module is the
|
|
25
|
+
* one place that knows where. It imports nothing but a type, which is the
|
|
26
|
+
* point: a page pays for these few lines, not for the capture.
|
|
27
|
+
*
|
|
28
|
+
* Consent can be decided before the script has booted (the script is loaded
|
|
29
|
+
* `async`), so a decision is also left where the script reads it at boot.
|
|
30
|
+
*/
|
|
31
|
+
/** Where a served script leaves its runtime. */
|
|
32
|
+
export declare const FIRST_TOUCH_PAGE_GLOBAL = "aglynFirstTouch";
|
|
33
|
+
/** Where a storage decision made before the script booted waits for it. */
|
|
34
|
+
export declare const FIRST_TOUCH_PAGE_STORAGE_GLOBAL = "__aglynFirstTouchStorage";
|
|
35
|
+
/** The runtime a served script booted on this page, or null. */
|
|
36
|
+
export declare function pageFirstTouchRuntime(): FirstTouchRuntime | null;
|
|
37
|
+
/**
|
|
38
|
+
* Tell the page's capture whether it may keep the record on the device:
|
|
39
|
+
* `true` to grant, `false` to refuse (and erase what it kept), `null` while
|
|
40
|
+
* the visitor's consent is unresolved. Reaches a runtime that is already
|
|
41
|
+
* running and waits for one that has not booted yet.
|
|
42
|
+
*/
|
|
43
|
+
export declare function setPageFirstTouchStorage(allowed: boolean | null): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The bridge between a SERVED capture script and the page's own code.
|
|
18
|
+
*
|
|
19
|
+
* A surface that includes the capture as a script tag has no import to call
|
|
20
|
+
* it through, and its consent code — which decides whether the record may be
|
|
21
|
+
* written to the device — runs in a bundle that never loaded the capture. So
|
|
22
|
+
* the served script leaves its runtime on `window`, and this module is the
|
|
23
|
+
* one place that knows where. It imports nothing but a type, which is the
|
|
24
|
+
* point: a page pays for these few lines, not for the capture.
|
|
25
|
+
*
|
|
26
|
+
* Consent can be decided before the script has booted (the script is loaded
|
|
27
|
+
* `async`), so a decision is also left where the script reads it at boot.
|
|
28
|
+
*/ /** Where a served script leaves its runtime. */ export const FIRST_TOUCH_PAGE_GLOBAL = 'aglynFirstTouch';
|
|
29
|
+
/** Where a storage decision made before the script booted waits for it. */ export const FIRST_TOUCH_PAGE_STORAGE_GLOBAL = '__aglynFirstTouchStorage';
|
|
30
|
+
function pageWindow() {
|
|
31
|
+
return typeof window === 'undefined' ? null : window;
|
|
32
|
+
}
|
|
33
|
+
/** The runtime a served script booted on this page, or null. */ export function pageFirstTouchRuntime() {
|
|
34
|
+
var _pageWindow;
|
|
35
|
+
const runtime = (_pageWindow = pageWindow()) == null ? void 0 : _pageWindow[FIRST_TOUCH_PAGE_GLOBAL];
|
|
36
|
+
return runtime && typeof runtime.read === 'function' && typeof runtime.setStorage === 'function' ? runtime : null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Tell the page's capture whether it may keep the record on the device:
|
|
40
|
+
* `true` to grant, `false` to refuse (and erase what it kept), `null` while
|
|
41
|
+
* the visitor's consent is unresolved. Reaches a runtime that is already
|
|
42
|
+
* running and waits for one that has not booted yet.
|
|
43
|
+
*/ export function setPageFirstTouchStorage(allowed) {
|
|
44
|
+
var _pageFirstTouchRuntime;
|
|
45
|
+
const page = pageWindow();
|
|
46
|
+
if (!page) return;
|
|
47
|
+
const value = allowed === true ? true : allowed === false ? false : null;
|
|
48
|
+
page[FIRST_TOUCH_PAGE_STORAGE_GLOBAL] = value;
|
|
49
|
+
(_pageFirstTouchRuntime = pageFirstTouchRuntime()) == null ? void 0 : _pageFirstTouchRuntime.setStorage(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=first-touch-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/first-touch/src/lib/first-touch-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { FirstTouchRuntime } from './first-touch'\n\n/**\n * The bridge between a SERVED capture script and the page's own code.\n *\n * A surface that includes the capture as a script tag has no import to call\n * it through, and its consent code — which decides whether the record may be\n * written to the device — runs in a bundle that never loaded the capture. So\n * the served script leaves its runtime on `window`, and this module is the\n * one place that knows where. It imports nothing but a type, which is the\n * point: a page pays for these few lines, not for the capture.\n *\n * Consent can be decided before the script has booted (the script is loaded\n * `async`), so a decision is also left where the script reads it at boot.\n */\n\n/** Where a served script leaves its runtime. */\nexport const FIRST_TOUCH_PAGE_GLOBAL = 'aglynFirstTouch'\n\n/** Where a storage decision made before the script booted waits for it. */\nexport const FIRST_TOUCH_PAGE_STORAGE_GLOBAL = '__aglynFirstTouchStorage'\n\nfunction pageWindow(): Record<string, unknown> | null {\n return typeof window === 'undefined'\n ? null\n : (window as unknown as Record<string, unknown>)\n}\n\n/** The runtime a served script booted on this page, or null. */\nexport function pageFirstTouchRuntime(): FirstTouchRuntime | null {\n const runtime = pageWindow()?.[FIRST_TOUCH_PAGE_GLOBAL] as\n | Partial<FirstTouchRuntime>\n | undefined\n return runtime &&\n typeof runtime.read === 'function' &&\n typeof runtime.setStorage === 'function'\n ? (runtime as FirstTouchRuntime)\n : null\n}\n\n/**\n * Tell the page's capture whether it may keep the record on the device:\n * `true` to grant, `false` to refuse (and erase what it kept), `null` while\n * the visitor's consent is unresolved. Reaches a runtime that is already\n * running and waits for one that has not booted yet.\n */\nexport function setPageFirstTouchStorage(allowed: boolean | null): void {\n const page = pageWindow()\n if (!page) return\n const value = allowed === true ? true : allowed === false ? false : null\n page[FIRST_TOUCH_PAGE_STORAGE_GLOBAL] = value\n pageFirstTouchRuntime()?.setStorage(value)\n}\n"],"names":["FIRST_TOUCH_PAGE_GLOBAL","FIRST_TOUCH_PAGE_STORAGE_GLOBAL","pageWindow","window","pageFirstTouchRuntime","runtime","read","setStorage","setPageFirstTouchStorage","allowed","page","value"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;CAYC,GAED,8CAA8C,GAC9C,OAAO,MAAMA,0BAA0B,kBAAiB;AAExD,yEAAyE,GACzE,OAAO,MAAMC,kCAAkC,2BAA0B;AAEzE,SAASC;IACP,OAAO,OAAOC,WAAW,cACrB,OACCA;AACP;AAEA,8DAA8D,GAC9D,OAAO,SAASC;QACEF;IAAhB,MAAMG,WAAUH,cAAAA,iCAAAA,WAAc,CAACF,wBAAwB;IAGvD,OAAOK,WACL,OAAOA,QAAQC,IAAI,KAAK,cACxB,OAAOD,QAAQE,UAAU,KAAK,aAC3BF,UACD;AACN;AAEA;;;;;CAKC,GACD,OAAO,SAASG,yBAAyBC,OAAuB;QAK9DL;IAJA,MAAMM,OAAOR;IACb,IAAI,CAACQ,MAAM;IACX,MAAMC,QAAQF,YAAY,OAAO,OAAOA,YAAY,QAAQ,QAAQ;IACpEC,IAAI,CAACT,gCAAgC,GAAGU;KACxCP,yBAAAA,4CAAAA,uBAAyBG,UAAU,CAACI;AACtC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type FirstTouchConfig } from './first-touch';
|
|
18
|
+
/**
|
|
19
|
+
* The capture as a script a page runs with no bundler: the kit's own source
|
|
20
|
+
* text, called with this surface's configuration.
|
|
21
|
+
*
|
|
22
|
+
* Served by the platform at one URL so that including the capture on a new
|
|
23
|
+
* surface is one tag — `<script src="…/api/first-touch" async>` — and the
|
|
24
|
+
* host registry, the hand-off endpoint and the storage decision arrive with
|
|
25
|
+
* it rather than being configured on each surface.
|
|
26
|
+
*
|
|
27
|
+
* Two page-side overrides, both read at boot:
|
|
28
|
+
*
|
|
29
|
+
* - `data-consent="pending"` on the tag holds the record in memory until the
|
|
30
|
+
* page's consent code grants storage — for a surface whose own consent tool
|
|
31
|
+
* decides after load;
|
|
32
|
+
* - a decision the page's code already left through
|
|
33
|
+
* `setPageFirstTouchStorage` before the script arrived wins over both the
|
|
34
|
+
* attribute and the served default, because it is the newest answer.
|
|
35
|
+
*
|
|
36
|
+
* Everything runs inside a `try`: a capture that throws must never cost the
|
|
37
|
+
* page it runs on.
|
|
38
|
+
*/
|
|
39
|
+
export declare function firstTouchScript(config: FirstTouchConfig): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { createFirstTouchKit } from "./first-touch.js";
|
|
17
|
+
import { FIRST_TOUCH_PAGE_GLOBAL, FIRST_TOUCH_PAGE_STORAGE_GLOBAL } from "./first-touch-page.js";
|
|
18
|
+
/**
|
|
19
|
+
* The capture as a script a page runs with no bundler: the kit's own source
|
|
20
|
+
* text, called with this surface's configuration.
|
|
21
|
+
*
|
|
22
|
+
* Served by the platform at one URL so that including the capture on a new
|
|
23
|
+
* surface is one tag — `<script src="…/api/first-touch" async>` — and the
|
|
24
|
+
* host registry, the hand-off endpoint and the storage decision arrive with
|
|
25
|
+
* it rather than being configured on each surface.
|
|
26
|
+
*
|
|
27
|
+
* Two page-side overrides, both read at boot:
|
|
28
|
+
*
|
|
29
|
+
* - `data-consent="pending"` on the tag holds the record in memory until the
|
|
30
|
+
* page's consent code grants storage — for a surface whose own consent tool
|
|
31
|
+
* decides after load;
|
|
32
|
+
* - a decision the page's code already left through
|
|
33
|
+
* `setPageFirstTouchStorage` before the script arrived wins over both the
|
|
34
|
+
* attribute and the served default, because it is the newest answer.
|
|
35
|
+
*
|
|
36
|
+
* Everything runs inside a `try`: a capture that throws must never cost the
|
|
37
|
+
* page it runs on.
|
|
38
|
+
*/ export function firstTouchScript(config) {
|
|
39
|
+
var _config_hosts, _config_handoffUrl;
|
|
40
|
+
const served = JSON.stringify({
|
|
41
|
+
hosts: [
|
|
42
|
+
...(_config_hosts = config.hosts) != null ? _config_hosts : []
|
|
43
|
+
],
|
|
44
|
+
storage: config.storage === undefined ? true : config.storage,
|
|
45
|
+
handoffUrl: (_config_handoffUrl = config.handoffUrl) != null ? _config_handoffUrl : null
|
|
46
|
+
})// Inline in a `<script>` element as well as served, so nothing in the
|
|
47
|
+
// configuration may close the element or break a pre-2019 parser.
|
|
48
|
+
.replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
49
|
+
return '(function(){try{' + `var c=${served};` + 'var w=window;var s=document.currentScript;' + "if(s&&s.getAttribute('data-consent')==='pending')c.storage=null;" + `var q=w[${JSON.stringify(FIRST_TOUCH_PAGE_STORAGE_GLOBAL)}];` + 'if(q===true||q===false||q===null)c.storage=q;' + `w[${JSON.stringify(FIRST_TOUCH_PAGE_GLOBAL)}]=(${createFirstTouchKit.toString()})().boot(c);` + '}catch(e){}})();';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=first-touch-script.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/first-touch/src/lib/first-touch-script.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createFirstTouchKit, type FirstTouchConfig } from './first-touch'\nimport {\n FIRST_TOUCH_PAGE_GLOBAL,\n FIRST_TOUCH_PAGE_STORAGE_GLOBAL,\n} from './first-touch-page'\n\n/**\n * The capture as a script a page runs with no bundler: the kit's own source\n * text, called with this surface's configuration.\n *\n * Served by the platform at one URL so that including the capture on a new\n * surface is one tag — `<script src=\"…/api/first-touch\" async>` — and the\n * host registry, the hand-off endpoint and the storage decision arrive with\n * it rather than being configured on each surface.\n *\n * Two page-side overrides, both read at boot:\n *\n * - `data-consent=\"pending\"` on the tag holds the record in memory until the\n * page's consent code grants storage — for a surface whose own consent tool\n * decides after load;\n * - a decision the page's code already left through\n * `setPageFirstTouchStorage` before the script arrived wins over both the\n * attribute and the served default, because it is the newest answer.\n *\n * Everything runs inside a `try`: a capture that throws must never cost the\n * page it runs on.\n */\nexport function firstTouchScript(config: FirstTouchConfig): string {\n const served = JSON.stringify({\n hosts: [...(config.hosts ?? [])],\n storage: config.storage === undefined ? true : config.storage,\n handoffUrl: config.handoffUrl ?? null,\n })\n // Inline in a `<script>` element as well as served, so nothing in the\n // configuration may close the element or break a pre-2019 parser.\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n return (\n '(function(){try{' +\n `var c=${served};` +\n 'var w=window;var s=document.currentScript;' +\n \"if(s&&s.getAttribute('data-consent')==='pending')c.storage=null;\" +\n `var q=w[${JSON.stringify(FIRST_TOUCH_PAGE_STORAGE_GLOBAL)}];` +\n 'if(q===true||q===false||q===null)c.storage=q;' +\n `w[${JSON.stringify(FIRST_TOUCH_PAGE_GLOBAL)}]=(${createFirstTouchKit.toString()})().boot(c);` +\n '}catch(e){}})();'\n )\n}\n"],"names":["createFirstTouchKit","FIRST_TOUCH_PAGE_GLOBAL","FIRST_TOUCH_PAGE_STORAGE_GLOBAL","firstTouchScript","config","served","JSON","stringify","hosts","storage","undefined","handoffUrl","replace","toString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAA+B,mBAAe;AAC1E,SACEC,uBAAuB,EACvBC,+BAA+B,QAC1B,wBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,iBAAiBC,MAAwB;QAEzCA,eAEAA;IAHd,MAAMC,SAASC,KAAKC,SAAS,CAAC;QAC5BC,OAAO;gBAAKJ,gBAAAA,OAAOI,KAAK,YAAZJ,gBAAgB,EAAE;SAAE;QAChCK,SAASL,OAAOK,OAAO,KAAKC,YAAY,OAAON,OAAOK,OAAO;QAC7DE,UAAU,GAAEP,qBAAAA,OAAOO,UAAU,YAAjBP,qBAAqB;IACnC,EACE,sEAAsE;IACtE,kEAAkE;KACjEQ,OAAO,CAAC,MAAM,WACdA,OAAO,CAAC,WAAW,WACnBA,OAAO,CAAC,WAAW;IACtB,OACE,qBACA,CAAC,MAAM,EAAEP,OAAO,CAAC,CAAC,GAClB,+CACA,qEACA,CAAC,QAAQ,EAAEC,KAAKC,SAAS,CAACL,iCAAiC,EAAE,CAAC,GAC9D,kDACA,CAAC,EAAE,EAAEI,KAAKC,SAAS,CAACN,yBAAyB,GAAG,EAAED,oBAAoBa,QAAQ,GAAG,YAAY,CAAC,GAC9F;AAEJ"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* First-touch capture: where a visitor first arrived from, kept on their
|
|
19
|
+
* device until an account is created and the platform writes it down.
|
|
20
|
+
*
|
|
21
|
+
* ## The one rule everything else follows from
|
|
22
|
+
*
|
|
23
|
+
* A visitor rarely signs up on the page they landed on, and the page they
|
|
24
|
+
* landed on is not always the marketing site. They find a guide on the docs
|
|
25
|
+
* host through a search engine, read the pricing page, then sign up on the
|
|
26
|
+
* console. Three hosts, and only the first one saw where they came from.
|
|
27
|
+
*
|
|
28
|
+
* So every surface the platform serves includes this capture, and every one
|
|
29
|
+
* of them agrees on which referrers are its own. **An internal referrer is
|
|
30
|
+
* never a first touch.** Only an external referrer, or a landing with no
|
|
31
|
+
* referrer at all, starts the record; a hop between two of our own hosts
|
|
32
|
+
* carries the record forward and never replaces it. "docs → pricing →
|
|
33
|
+
* signup" therefore still reports the search engine that started it.
|
|
34
|
+
*
|
|
35
|
+
* ## Where the record lives
|
|
36
|
+
*
|
|
37
|
+
* - **A cookie on the registrable domain** of the surface (`.example.com`),
|
|
38
|
+
* found by asking the browser rather than by carrying a public-suffix list:
|
|
39
|
+
* the broadest domain that accepts a cookie is the registrable one. Every
|
|
40
|
+
* subdomain, and so every console door under it, reads the same value.
|
|
41
|
+
* - **`sessionStorage`**, when the browser refuses the cookie. It dies with
|
|
42
|
+
* the tab and does not cross subdomains, which is the price of a browser
|
|
43
|
+
* that refuses storage.
|
|
44
|
+
* - **Memory**, while the visitor's consent is unresolved or has been refused
|
|
45
|
+
* for this surface. Nothing is written to the device. A link to another of
|
|
46
|
+
* our hosts can still carry the record, sealed, in its URL (below).
|
|
47
|
+
*
|
|
48
|
+
* The cookie is re-written on every load with a fresh lifetime, because some
|
|
49
|
+
* browsers cap the life of a script-written cookie at a week of inactivity;
|
|
50
|
+
* the VALUE never changes once set.
|
|
51
|
+
*
|
|
52
|
+
* ## Crossing to a host the cookie cannot reach
|
|
53
|
+
*
|
|
54
|
+
* A surface on a different registrable domain, or any hop made while the
|
|
55
|
+
* record is held in memory, cannot read the cookie. For those links the
|
|
56
|
+
* capture asks the platform to SEAL the record (an HMAC over it and an
|
|
57
|
+
* expiry, signed with a secret no page holds) and appends the sealed token as
|
|
58
|
+
* `_ft` at the moment the link is followed. The receiving surface strips the
|
|
59
|
+
* parameter from its address bar at once, asks the platform to OPEN it, and
|
|
60
|
+
* adopts what comes back. Adoption is a merge, and the merge keeps the
|
|
61
|
+
* earlier record, so replaying a token can never move a first touch later.
|
|
62
|
+
*
|
|
63
|
+
* What the seal proves is narrow and worth stating exactly: that this install
|
|
64
|
+
* produced the record, recently, and nobody edited it on the way. It does not
|
|
65
|
+
* prove the record is TRUE — a visitor can put any `utm_*` they like on a URL
|
|
66
|
+
* and always could. Attribution is a label, and it grants nothing.
|
|
67
|
+
*
|
|
68
|
+
* ## What is never recorded
|
|
69
|
+
*
|
|
70
|
+
* No identifier. The record says how a visit began, and two visitors who
|
|
71
|
+
* arrived the same way carry the same record give or take a timestamp. Click
|
|
72
|
+
* ids are kept as PRESENCE only — "this arrived from a paid click" — and their
|
|
73
|
+
* values never leave the URL they came on. Referrers are kept as a HOST, never
|
|
74
|
+
* a path, because a referring path can carry a search query or an account
|
|
75
|
+
* page. `utm_*` values are trimmed, refused when shaped like an email
|
|
76
|
+
* address, and capped, the same scrub the signup campaign parser applies.
|
|
77
|
+
*
|
|
78
|
+
* ## Why the whole runtime is one function
|
|
79
|
+
*
|
|
80
|
+
* The same code must run as a module a Next app imports and as a script tag
|
|
81
|
+
* on a page no bundler touches (a hosted forum, a status page). So the
|
|
82
|
+
* runtime is ONE self-contained function, {@link createFirstTouchKit}, that
|
|
83
|
+
* references nothing outside itself: the served script is that function's own
|
|
84
|
+
* source text followed by a call to it. The constraint that keeps it working
|
|
85
|
+
* is the same one `next-themes` lives under — no imports, no module-level
|
|
86
|
+
* values, and no syntax a compiler would lower into a shared helper (object
|
|
87
|
+
* spread, classes, `async`). `first-touch-script.spec.ts` runs the
|
|
88
|
+
* stringified function in a bare context to hold that.
|
|
89
|
+
*/
|
|
90
|
+
/** Click identifiers whose PRESENCE the record keeps; their values never leave the URL. */
|
|
91
|
+
export declare const FIRST_TOUCH_CLICK_IDS: readonly ["gclid", "fbclid", "msclkid"];
|
|
92
|
+
export type FirstTouchClickId = (typeof FIRST_TOUCH_CLICK_IDS)[number];
|
|
93
|
+
/** The `utm_*` parameters the record keeps, named without their prefix. */
|
|
94
|
+
export declare const FIRST_TOUCH_UTM_KEYS: readonly ["source", "medium", "campaign", "content", "term"];
|
|
95
|
+
export type FirstTouchUtmKey = (typeof FIRST_TOUCH_UTM_KEYS)[number];
|
|
96
|
+
export type FirstTouchUtm = Partial<Record<FirstTouchUtmKey, string>>;
|
|
97
|
+
/** The cookie the record is kept in, on the surface's registrable domain. */
|
|
98
|
+
export declare const FIRST_TOUCH_COOKIE = "aglyn_ft";
|
|
99
|
+
/** The query parameter a sealed hand-off rides on. */
|
|
100
|
+
export declare const FIRST_TOUCH_HANDOFF_PARAM = "_ft";
|
|
101
|
+
/** How a visit began, as the capture records it. */
|
|
102
|
+
export interface FirstTouch {
|
|
103
|
+
/** Record format. A reader refuses any other value rather than guess. */
|
|
104
|
+
v: 1;
|
|
105
|
+
/** When the visitor landed, epoch milliseconds. */
|
|
106
|
+
at: number;
|
|
107
|
+
/** The host of the first page they landed on. */
|
|
108
|
+
host: string;
|
|
109
|
+
/** That page's path, without its query string or fragment. */
|
|
110
|
+
path: string;
|
|
111
|
+
/** The EXTERNAL host that sent them, or null when nothing external did. */
|
|
112
|
+
ref: string | null;
|
|
113
|
+
/**
|
|
114
|
+
* One of our own hosts they arrived from before any surface had recorded
|
|
115
|
+
* them — the tell of a surface that does not include the capture yet.
|
|
116
|
+
*/
|
|
117
|
+
via?: string;
|
|
118
|
+
/** The `utm_*` parameters the landing URL carried. */
|
|
119
|
+
utm?: FirstTouchUtm;
|
|
120
|
+
/** Which click identifiers the landing URL carried. */
|
|
121
|
+
click?: FirstTouchClickId[];
|
|
122
|
+
}
|
|
123
|
+
/** What {@link FirstTouchKit.buildFirstTouch} reads a landing from. */
|
|
124
|
+
export interface FirstTouchLanding {
|
|
125
|
+
/** The URL the visitor landed on. */
|
|
126
|
+
href: string;
|
|
127
|
+
/** `document.referrer` at that moment; empty when there was none. */
|
|
128
|
+
referrer?: string | null;
|
|
129
|
+
/** The hosts this install serves itself — see {@link FirstTouchConfig.hosts}. */
|
|
130
|
+
hosts: readonly string[];
|
|
131
|
+
/** The time to stamp, epoch milliseconds. */
|
|
132
|
+
now: number;
|
|
133
|
+
}
|
|
134
|
+
/** How a surface configures the capture. */
|
|
135
|
+
export interface FirstTouchConfig {
|
|
136
|
+
/**
|
|
137
|
+
* The hosts this install serves itself. An entry is an exact host
|
|
138
|
+
* (`example.com`) or a wildcard for every subdomain of one at any depth
|
|
139
|
+
* (`*.example.com`, which does not match `example.com` itself). A leading
|
|
140
|
+
* `!` EXCLUDES what the rest of the entry names, whatever else matches it:
|
|
141
|
+
* `!*.sites.example.com` keeps customer sites served under the operator's
|
|
142
|
+
* own domain external. A referrer on an included host is internal, and the
|
|
143
|
+
* capture runs only on a page whose host is included.
|
|
144
|
+
*/
|
|
145
|
+
hosts: readonly string[];
|
|
146
|
+
/**
|
|
147
|
+
* Whether this surface may keep the record on the device. `null` means the
|
|
148
|
+
* visitor's consent is not resolved yet: the record is held in memory and
|
|
149
|
+
* written the moment {@link FirstTouchRuntime.setStorage} grants it.
|
|
150
|
+
* Omitted means true, the posture of a surface with no consent gate.
|
|
151
|
+
*/
|
|
152
|
+
storage?: boolean | null;
|
|
153
|
+
/**
|
|
154
|
+
* The platform endpoint that seals and opens hand-off tokens: absolute, or
|
|
155
|
+
* a path on the current origin. Without it no link is decorated and no
|
|
156
|
+
* token is adopted, and the cookie alone carries the record.
|
|
157
|
+
*/
|
|
158
|
+
handoffUrl?: string | null;
|
|
159
|
+
}
|
|
160
|
+
/** Where the record currently lives on this page. */
|
|
161
|
+
export type FirstTouchTier = 'cookie' | 'session' | 'memory';
|
|
162
|
+
/** The handle a booted capture returns. */
|
|
163
|
+
export interface FirstTouchRuntime {
|
|
164
|
+
/** The first touch as this page knows it, or null. */
|
|
165
|
+
read(): FirstTouch | null;
|
|
166
|
+
/** Grant, refuse or un-resolve device storage for this surface. */
|
|
167
|
+
setStorage(allowed: boolean | null): void;
|
|
168
|
+
/** Where the record lives right now, or null when there is none. */
|
|
169
|
+
tier(): FirstTouchTier | null;
|
|
170
|
+
}
|
|
171
|
+
/** Everything the capture does, as one closure — see the file comment for why. */
|
|
172
|
+
export interface FirstTouchKit {
|
|
173
|
+
/** A bare lowercase host, or '' when the value is not one. */
|
|
174
|
+
normalizeHost(value: unknown): string;
|
|
175
|
+
/** Whether `host` is one of `hosts` — see {@link FirstTouchConfig.hosts}. */
|
|
176
|
+
isFirstPartyHost(host: unknown, hosts: readonly string[]): boolean;
|
|
177
|
+
/** The touch a landing describes, or null when the URL is not a web page. */
|
|
178
|
+
buildFirstTouch(landing: FirstTouchLanding): FirstTouch | null;
|
|
179
|
+
/**
|
|
180
|
+
* Rebuild a record from an untrusted value, keeping only known fields, each
|
|
181
|
+
* re-scrubbed; null when it is not a record. `now`, when given, refuses a
|
|
182
|
+
* record stamped more than a day into the future.
|
|
183
|
+
*/
|
|
184
|
+
sanitizeFirstTouch(value: unknown, now?: number): FirstTouch | null;
|
|
185
|
+
/** The first of two records: the earlier `at` wins, and a tie keeps `a`. */
|
|
186
|
+
mergeFirstTouch(a: FirstTouch | null | undefined, b: FirstTouch | null | undefined): FirstTouch | null;
|
|
187
|
+
/** The cookie-safe text form of a record. */
|
|
188
|
+
encodeFirstTouch(touch: FirstTouch): string;
|
|
189
|
+
/** A record from its cookie text, sanitized; null for anything else. */
|
|
190
|
+
decodeFirstTouch(value: unknown): FirstTouch | null;
|
|
191
|
+
/** The record a `Cookie` header carries, for a server that receives one. */
|
|
192
|
+
readFirstTouchCookie(cookieHeader: string | null | undefined): FirstTouch | null;
|
|
193
|
+
/** Start capturing on this page. Idempotent; a second call updates the config. */
|
|
194
|
+
boot(config: FirstTouchConfig): FirstTouchRuntime;
|
|
195
|
+
/** The booted runtime's record, or whatever the device holds when none booted. */
|
|
196
|
+
read(): FirstTouch | null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build the capture. Self-contained by construction: nothing in the body may
|
|
200
|
+
* reference a value declared outside it (types are erased and do not count).
|
|
201
|
+
*/
|
|
202
|
+
export declare function createFirstTouchKit(): FirstTouchKit;
|
|
203
|
+
export declare const normalizeFirstTouchHost: (value: unknown) => string;
|
|
204
|
+
export declare const isFirstPartyHost: (host: unknown, hosts: readonly string[]) => boolean;
|
|
205
|
+
export declare const buildFirstTouch: (landing: FirstTouchLanding) => FirstTouch | null;
|
|
206
|
+
export declare const sanitizeFirstTouch: (value: unknown, now?: number) => FirstTouch | null;
|
|
207
|
+
export declare const mergeFirstTouch: (a: FirstTouch | null | undefined, b: FirstTouch | null | undefined) => FirstTouch | null;
|
|
208
|
+
export declare const encodeFirstTouch: (touch: FirstTouch) => string;
|
|
209
|
+
export declare const decodeFirstTouch: (value: unknown) => FirstTouch | null;
|
|
210
|
+
export declare const readFirstTouchCookie: (cookieHeader: string | null | undefined) => FirstTouch | null;
|
|
211
|
+
export declare const bootFirstTouch: (config: FirstTouchConfig) => FirstTouchRuntime;
|
|
212
|
+
export declare const readFirstTouch: () => FirstTouch | null;
|