@aglyn/aglyn 1.0.0-beta.176 → 1.0.0-beta.177
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/package.json +11 -10
- package/src/lib/app-utils/account-acquisition.d.ts +164 -0
- package/src/lib/app-utils/account-acquisition.js +300 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -0
- package/src/lib/app-utils/first-party-hosts.d.ts +86 -0
- package/src/lib/app-utils/first-party-hosts.js +143 -0
- package/src/lib/app-utils/first-party-hosts.js.map +1 -0
- package/src/lib/app-utils/visitor-consent.d.ts +9 -0
- package/src/lib/app-utils/visitor-consent.js +19 -4
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +9 -0
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-matches.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-person-matches.js +103 -0
- package/src/lib/plugin-manager/plugin-person-matches.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.177",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,15 +33,16 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-
|
|
41
|
-
"@aglyn/shared-util-
|
|
42
|
-
"@aglyn/shared-util-
|
|
43
|
-
"@aglyn/shared-util-
|
|
44
|
-
"@aglyn/shared-util-
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.177",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.177",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.177",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.177",
|
|
40
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.177",
|
|
41
|
+
"@aglyn/shared-util-http": "1.0.0-beta.177",
|
|
42
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.177",
|
|
43
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.177",
|
|
44
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.177",
|
|
45
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.177",
|
|
45
46
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
46
47
|
"@msgpack/msgpack": "^3.1.3",
|
|
47
48
|
"@types/unist": "^3.0.3",
|
|
@@ -0,0 +1,164 @@
|
|
|
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 AcquisitionChannel, type FirstTouchClickId, type FirstTouchUtm } from '@aglyn/shared-util-first-touch';
|
|
18
|
+
/**
|
|
19
|
+
* Where an account came from, written ONCE on the account when it is created
|
|
20
|
+
* (AGL-3289) — `users/{uid}.acquisition`, and on the organization created in
|
|
21
|
+
* the same flow as `orgs/{orgId}.acquisition`.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this exists
|
|
24
|
+
*
|
|
25
|
+
* Answering "where did this person come from?" for one new workspace took a
|
|
26
|
+
* sales session twenty-five minutes across five tools: the staff user page
|
|
27
|
+
* had no source, Firestore held nothing beyond names, and the answer — a
|
|
28
|
+
* review site's referral — was finally found in an analytics property and in
|
|
29
|
+
* the marketing host's daily referrer counts. The platform never stored the
|
|
30
|
+
* fact on the account it was about. This record is that fact, read from one
|
|
31
|
+
* card on the staff user and organization pages.
|
|
32
|
+
*
|
|
33
|
+
* ## What writes it, and what never may
|
|
34
|
+
*
|
|
35
|
+
* The capture library keeps the first touch on the visitor's device; the
|
|
36
|
+
* platform turns it into this record at account creation, server-side, and
|
|
37
|
+
* the rules refuse the field to every client — owner and staff alike. A
|
|
38
|
+
* record the account holder could restate would be a record nobody could
|
|
39
|
+
* trust. It is never overwritten: a later touch, if one is ever kept, goes
|
|
40
|
+
* BESIDE it as its own field.
|
|
41
|
+
*
|
|
42
|
+
* Every field here is built from the untrusted touch by re-scrubbing it, the
|
|
43
|
+
* same way the capture scrubbed it on the device, and nothing the record says
|
|
44
|
+
* grants anything: it is a label for a staff reader.
|
|
45
|
+
*/
|
|
46
|
+
/** The field on `users/{uid}` and `orgs/{orgId}`. */
|
|
47
|
+
export declare const ACCOUNT_ACQUISITION_FIELD = "acquisition";
|
|
48
|
+
/**
|
|
49
|
+
* How an account was created: the signup form with a password, the signup
|
|
50
|
+
* form through Google, accepting an invitation, or single sign-on.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ACQUISITION_DOORS: readonly ["signup-password", "signup-google", "invite", "sso"];
|
|
53
|
+
export type AcquisitionDoor = (typeof ACQUISITION_DOORS)[number] | 'unknown';
|
|
54
|
+
/** What wrote a record: a door at creation, an organization copying its creator's, or the backfill. */
|
|
55
|
+
export type AcquisitionRecordedBy = 'signup' | 'sso' | 'org-creation' | 'backfill';
|
|
56
|
+
/** Where the account-creating request came from, as the edge reported it. */
|
|
57
|
+
export interface AcquisitionGeo {
|
|
58
|
+
country: string | null;
|
|
59
|
+
region: string | null;
|
|
60
|
+
city: string | null;
|
|
61
|
+
}
|
|
62
|
+
/** The stored record. */
|
|
63
|
+
export interface AccountAcquisition {
|
|
64
|
+
v: 1;
|
|
65
|
+
/**
|
|
66
|
+
* Where the visit came from, spelled the way reports spell it — a
|
|
67
|
+
* `utm_source`, a referring host, `(direct)` — or `unknown` when nothing was
|
|
68
|
+
* captured.
|
|
69
|
+
*/
|
|
70
|
+
source: string;
|
|
71
|
+
/** `utm_medium`, or what the channel implies; null when unknown. */
|
|
72
|
+
medium: string | null;
|
|
73
|
+
/** `utm_campaign`, when the landing URL named one. */
|
|
74
|
+
campaign: string | null;
|
|
75
|
+
/** The channel, from the platform's own rule table. */
|
|
76
|
+
channel: AcquisitionChannel;
|
|
77
|
+
/** When the first visit landed; null when it was never captured. */
|
|
78
|
+
capturedAt: number | null;
|
|
79
|
+
/** The first page the visitor landed on. */
|
|
80
|
+
landing: {
|
|
81
|
+
host: string;
|
|
82
|
+
path: string;
|
|
83
|
+
} | null;
|
|
84
|
+
/** The external host that sent them. */
|
|
85
|
+
referrerHost: string | null;
|
|
86
|
+
/** One of the platform's own hosts they came from before anything captured them. */
|
|
87
|
+
viaHost: string | null;
|
|
88
|
+
/** Every `utm_*` the landing URL carried. */
|
|
89
|
+
utm: FirstTouchUtm | null;
|
|
90
|
+
/** Which ad click ids the landing URL carried — presence only. */
|
|
91
|
+
clickIds: FirstTouchClickId[];
|
|
92
|
+
/** How the account was created. */
|
|
93
|
+
door: AcquisitionDoor;
|
|
94
|
+
/** The sign-in provider it was created with: `password`, `google.com`, `saml.…`. */
|
|
95
|
+
provider: string | null;
|
|
96
|
+
/** The organization whose invitation the person signed up to accept. */
|
|
97
|
+
invitedToOrgId: string | null;
|
|
98
|
+
/** Where the account-creating request came from. */
|
|
99
|
+
geo: AcquisitionGeo | null;
|
|
100
|
+
/** When the auth record says the account was created. */
|
|
101
|
+
accountCreatedAt: number | null;
|
|
102
|
+
/** When this record was written. */
|
|
103
|
+
recordedAt: number;
|
|
104
|
+
recordedBy: AcquisitionRecordedBy;
|
|
105
|
+
/** On an organization's copy: the account it was copied from. */
|
|
106
|
+
copiedFromUid?: string;
|
|
107
|
+
}
|
|
108
|
+
/** What a door knows when it records an account. */
|
|
109
|
+
export interface AccountAcquisitionInput {
|
|
110
|
+
/** The first touch the capture kept — untrusted, from the device. */
|
|
111
|
+
touch: unknown;
|
|
112
|
+
door: unknown;
|
|
113
|
+
provider?: unknown;
|
|
114
|
+
geo?: unknown;
|
|
115
|
+
invitedToOrgId?: unknown;
|
|
116
|
+
/** When the auth record says the account was created. */
|
|
117
|
+
accountCreatedAtMs?: number | null;
|
|
118
|
+
recordedBy: AcquisitionRecordedBy;
|
|
119
|
+
nowMs: number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The record for an account being created now.
|
|
123
|
+
*
|
|
124
|
+
* A missing or unusable touch still produces a record — `source: 'unknown'`
|
|
125
|
+
* with whatever the door knows — because "we asked and nothing came back" is
|
|
126
|
+
* a fact about the account too, and a card that renders blank would read as
|
|
127
|
+
* "never asked".
|
|
128
|
+
*/
|
|
129
|
+
export declare function buildAccountAcquisition(input: AccountAcquisitionInput): AccountAcquisition;
|
|
130
|
+
/** A record for an account nothing was captured for. */
|
|
131
|
+
export declare function unknownAccountAcquisition(input: {
|
|
132
|
+
recordedAt: number;
|
|
133
|
+
recordedBy: AcquisitionRecordedBy;
|
|
134
|
+
door?: AcquisitionDoor;
|
|
135
|
+
provider?: string | null;
|
|
136
|
+
invitedToOrgId?: string | null;
|
|
137
|
+
geo?: AcquisitionGeo | null;
|
|
138
|
+
accountCreatedAt?: number | null;
|
|
139
|
+
}): AccountAcquisition;
|
|
140
|
+
/**
|
|
141
|
+
* The organization's copy of its creator's record, or an `unknown` one naming
|
|
142
|
+
* the creator when the account has none — so an organization's card is never
|
|
143
|
+
* blank either, and always says whose record it is.
|
|
144
|
+
*/
|
|
145
|
+
export declare function organizationAcquisition(creatorRecord: unknown, creatorUid: string, nowMs: number): AccountAcquisition;
|
|
146
|
+
/**
|
|
147
|
+
* A stored record as a reader should trust it: re-validated field by field,
|
|
148
|
+
* so a document written by an older version, a backfill or a hand edit still
|
|
149
|
+
* renders, and renders nothing it should not. Null when the value is not a
|
|
150
|
+
* record at all.
|
|
151
|
+
*/
|
|
152
|
+
export declare function readAccountAcquisition(value: unknown): AccountAcquisition | null;
|
|
153
|
+
/** A sign-in provider id as a person reads it. */
|
|
154
|
+
export declare function providerLabel(provider: string | null | undefined): string | null;
|
|
155
|
+
/**
|
|
156
|
+
* The record as one sentence a staff member reads at a glance:
|
|
157
|
+
* "Referral from g2.com → /pricing → signed up with password".
|
|
158
|
+
*
|
|
159
|
+
* The landing is a bare path when it was on `primaryHost` — the platform's own
|
|
160
|
+
* home — and host-and-path anywhere else, so a docs landing says so.
|
|
161
|
+
*/
|
|
162
|
+
export declare function describeAccountAcquisition(record: AccountAcquisition | null | undefined, options?: {
|
|
163
|
+
primaryHost?: string | null;
|
|
164
|
+
}): string;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { classifyChannel, displayHost, sanitizeFirstTouch, sourceAndMedium } from "@aglyn/shared-util-first-touch";
|
|
18
|
+
/**
|
|
19
|
+
* Where an account came from, written ONCE on the account when it is created
|
|
20
|
+
* (AGL-3289) — `users/{uid}.acquisition`, and on the organization created in
|
|
21
|
+
* the same flow as `orgs/{orgId}.acquisition`.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this exists
|
|
24
|
+
*
|
|
25
|
+
* Answering "where did this person come from?" for one new workspace took a
|
|
26
|
+
* sales session twenty-five minutes across five tools: the staff user page
|
|
27
|
+
* had no source, Firestore held nothing beyond names, and the answer — a
|
|
28
|
+
* review site's referral — was finally found in an analytics property and in
|
|
29
|
+
* the marketing host's daily referrer counts. The platform never stored the
|
|
30
|
+
* fact on the account it was about. This record is that fact, read from one
|
|
31
|
+
* card on the staff user and organization pages.
|
|
32
|
+
*
|
|
33
|
+
* ## What writes it, and what never may
|
|
34
|
+
*
|
|
35
|
+
* The capture library keeps the first touch on the visitor's device; the
|
|
36
|
+
* platform turns it into this record at account creation, server-side, and
|
|
37
|
+
* the rules refuse the field to every client — owner and staff alike. A
|
|
38
|
+
* record the account holder could restate would be a record nobody could
|
|
39
|
+
* trust. It is never overwritten: a later touch, if one is ever kept, goes
|
|
40
|
+
* BESIDE it as its own field.
|
|
41
|
+
*
|
|
42
|
+
* Every field here is built from the untrusted touch by re-scrubbing it, the
|
|
43
|
+
* same way the capture scrubbed it on the device, and nothing the record says
|
|
44
|
+
* grants anything: it is a label for a staff reader.
|
|
45
|
+
*/ /** The field on `users/{uid}` and `orgs/{orgId}`. */ export const ACCOUNT_ACQUISITION_FIELD = 'acquisition';
|
|
46
|
+
/**
|
|
47
|
+
* How an account was created: the signup form with a password, the signup
|
|
48
|
+
* form through Google, accepting an invitation, or single sign-on.
|
|
49
|
+
*/ export const ACQUISITION_DOORS = [
|
|
50
|
+
'signup-password',
|
|
51
|
+
'signup-google',
|
|
52
|
+
'invite',
|
|
53
|
+
'sso'
|
|
54
|
+
];
|
|
55
|
+
const MAX_LABEL = 100;
|
|
56
|
+
const PROVIDER_SHAPE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
57
|
+
const RECORDED_BY = [
|
|
58
|
+
'signup',
|
|
59
|
+
'sso',
|
|
60
|
+
'org-creation',
|
|
61
|
+
'backfill'
|
|
62
|
+
];
|
|
63
|
+
function label(value, max = MAX_LABEL) {
|
|
64
|
+
if (typeof value !== 'string') return null;
|
|
65
|
+
const clean = value.replace(/[\u0000-\u001f\u007f]/g, '').trim() // eslint-disable-line no-control-regex
|
|
66
|
+
;
|
|
67
|
+
return clean ? clean.slice(0, max) : null;
|
|
68
|
+
}
|
|
69
|
+
function doorOf(value) {
|
|
70
|
+
return ACQUISITION_DOORS.includes(value) ? value : 'unknown';
|
|
71
|
+
}
|
|
72
|
+
function providerOf(value) {
|
|
73
|
+
return typeof value === 'string' && PROVIDER_SHAPE.test(value.trim()) ? value.trim().toLowerCase() : null;
|
|
74
|
+
}
|
|
75
|
+
function geoOf(value) {
|
|
76
|
+
var _ref;
|
|
77
|
+
var _label;
|
|
78
|
+
if (!value || typeof value !== 'object') return null;
|
|
79
|
+
const raw = value;
|
|
80
|
+
const geo = {
|
|
81
|
+
country: (_ref = (_label = label(raw['country'], 8)) == null ? void 0 : _label.toUpperCase()) != null ? _ref : null,
|
|
82
|
+
region: label(raw['region'], 64),
|
|
83
|
+
city: label(raw['city'], 64)
|
|
84
|
+
};
|
|
85
|
+
return geo.country || geo.region || geo.city ? geo : null;
|
|
86
|
+
}
|
|
87
|
+
function millisOf(value) {
|
|
88
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
89
|
+
}
|
|
90
|
+
function uidOf(value) {
|
|
91
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value) ? value : null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The record for an account being created now.
|
|
95
|
+
*
|
|
96
|
+
* A missing or unusable touch still produces a record — `source: 'unknown'`
|
|
97
|
+
* with whatever the door knows — because "we asked and nothing came back" is
|
|
98
|
+
* a fact about the account too, and a card that renders blank would read as
|
|
99
|
+
* "never asked".
|
|
100
|
+
*/ export function buildAccountAcquisition(input) {
|
|
101
|
+
var _ref, _touch_via, _touch_utm, _touch_click;
|
|
102
|
+
var _touch_utm1;
|
|
103
|
+
const touch = sanitizeFirstTouch(input.touch, input.nowMs);
|
|
104
|
+
const base = {
|
|
105
|
+
door: doorOf(input.door),
|
|
106
|
+
provider: providerOf(input.provider),
|
|
107
|
+
invitedToOrgId: uidOf(input.invitedToOrgId),
|
|
108
|
+
geo: geoOf(input.geo),
|
|
109
|
+
accountCreatedAt: millisOf(input.accountCreatedAtMs),
|
|
110
|
+
recordedAt: input.nowMs,
|
|
111
|
+
recordedBy: input.recordedBy
|
|
112
|
+
};
|
|
113
|
+
if (!touch) return unknownAccountAcquisition(base);
|
|
114
|
+
const { source, medium } = sourceAndMedium(touch);
|
|
115
|
+
return _extends({
|
|
116
|
+
v: 1,
|
|
117
|
+
source,
|
|
118
|
+
medium,
|
|
119
|
+
campaign: (_ref = (_touch_utm1 = touch.utm) == null ? void 0 : _touch_utm1.campaign) != null ? _ref : null,
|
|
120
|
+
channel: classifyChannel(touch),
|
|
121
|
+
capturedAt: touch.at,
|
|
122
|
+
landing: {
|
|
123
|
+
host: touch.host,
|
|
124
|
+
path: touch.path
|
|
125
|
+
},
|
|
126
|
+
referrerHost: touch.ref,
|
|
127
|
+
viaHost: (_touch_via = touch.via) != null ? _touch_via : null,
|
|
128
|
+
utm: (_touch_utm = touch.utm) != null ? _touch_utm : null,
|
|
129
|
+
clickIds: (_touch_click = touch.click) != null ? _touch_click : []
|
|
130
|
+
}, base);
|
|
131
|
+
}
|
|
132
|
+
/** A record for an account nothing was captured for. */ export function unknownAccountAcquisition(input) {
|
|
133
|
+
var _input_door, _input_provider, _input_invitedToOrgId, _input_geo, _input_accountCreatedAt;
|
|
134
|
+
return {
|
|
135
|
+
v: 1,
|
|
136
|
+
source: 'unknown',
|
|
137
|
+
medium: null,
|
|
138
|
+
campaign: null,
|
|
139
|
+
channel: 'unknown',
|
|
140
|
+
capturedAt: null,
|
|
141
|
+
landing: null,
|
|
142
|
+
referrerHost: null,
|
|
143
|
+
viaHost: null,
|
|
144
|
+
utm: null,
|
|
145
|
+
clickIds: [],
|
|
146
|
+
door: (_input_door = input.door) != null ? _input_door : 'unknown',
|
|
147
|
+
provider: (_input_provider = input.provider) != null ? _input_provider : null,
|
|
148
|
+
invitedToOrgId: (_input_invitedToOrgId = input.invitedToOrgId) != null ? _input_invitedToOrgId : null,
|
|
149
|
+
geo: (_input_geo = input.geo) != null ? _input_geo : null,
|
|
150
|
+
accountCreatedAt: (_input_accountCreatedAt = input.accountCreatedAt) != null ? _input_accountCreatedAt : null,
|
|
151
|
+
recordedAt: input.recordedAt,
|
|
152
|
+
recordedBy: input.recordedBy
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The organization's copy of its creator's record, or an `unknown` one naming
|
|
157
|
+
* the creator when the account has none — so an organization's card is never
|
|
158
|
+
* blank either, and always says whose record it is.
|
|
159
|
+
*/ export function organizationAcquisition(creatorRecord, creatorUid, nowMs) {
|
|
160
|
+
const record = readAccountAcquisition(creatorRecord);
|
|
161
|
+
const copied = record ? _extends({}, record) : unknownAccountAcquisition({
|
|
162
|
+
recordedAt: nowMs,
|
|
163
|
+
recordedBy: 'org-creation'
|
|
164
|
+
});
|
|
165
|
+
delete copied.copiedFromUid;
|
|
166
|
+
return _extends({}, copied, {
|
|
167
|
+
copiedFromUid: creatorUid
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A stored record as a reader should trust it: re-validated field by field,
|
|
172
|
+
* so a document written by an older version, a backfill or a hand edit still
|
|
173
|
+
* renders, and renders nothing it should not. Null when the value is not a
|
|
174
|
+
* record at all.
|
|
175
|
+
*/ export function readAccountAcquisition(value) {
|
|
176
|
+
var _label;
|
|
177
|
+
if (!value || typeof value !== 'object') return null;
|
|
178
|
+
const raw = value;
|
|
179
|
+
const source = label(raw['source']);
|
|
180
|
+
if (!source) return null;
|
|
181
|
+
const channel = raw['channel'];
|
|
182
|
+
const landing = raw['landing'];
|
|
183
|
+
const utmRaw = raw['utm'];
|
|
184
|
+
const utm = {};
|
|
185
|
+
for (const key of [
|
|
186
|
+
'source',
|
|
187
|
+
'medium',
|
|
188
|
+
'campaign',
|
|
189
|
+
'content',
|
|
190
|
+
'term'
|
|
191
|
+
]){
|
|
192
|
+
const cleaned = label(utmRaw == null ? void 0 : utmRaw[key]);
|
|
193
|
+
if (cleaned) utm[key] = cleaned;
|
|
194
|
+
}
|
|
195
|
+
const clickIds = Array.isArray(raw['clickIds']) ? [
|
|
196
|
+
'gclid',
|
|
197
|
+
'fbclid',
|
|
198
|
+
'msclkid'
|
|
199
|
+
].filter((id)=>raw['clickIds'].includes(id)) : [];
|
|
200
|
+
const recordedBy = RECORDED_BY.includes(raw['recordedBy']) ? raw['recordedBy'] : 'backfill';
|
|
201
|
+
const record = {
|
|
202
|
+
v: 1,
|
|
203
|
+
source,
|
|
204
|
+
medium: label(raw['medium']),
|
|
205
|
+
campaign: label(raw['campaign']),
|
|
206
|
+
channel: typeof channel === 'string' && [
|
|
207
|
+
'organic-search',
|
|
208
|
+
'paid-search',
|
|
209
|
+
'social',
|
|
210
|
+
'referral',
|
|
211
|
+
'email',
|
|
212
|
+
'direct',
|
|
213
|
+
'unknown'
|
|
214
|
+
].includes(channel) ? channel : 'unknown',
|
|
215
|
+
capturedAt: typeof raw['capturedAt'] === 'number' && raw['capturedAt'] > 0 ? raw['capturedAt'] : null,
|
|
216
|
+
landing: landing && label(landing['host'], 253) ? {
|
|
217
|
+
host: label(landing['host'], 253),
|
|
218
|
+
path: (_label = label(landing['path'], 200)) != null ? _label : '/'
|
|
219
|
+
} : null,
|
|
220
|
+
referrerHost: label(raw['referrerHost'], 253),
|
|
221
|
+
viaHost: label(raw['viaHost'], 253),
|
|
222
|
+
utm: Object.keys(utm).length ? utm : null,
|
|
223
|
+
clickIds,
|
|
224
|
+
door: doorOf(raw['door']),
|
|
225
|
+
provider: providerOf(raw['provider']),
|
|
226
|
+
invitedToOrgId: uidOf(raw['invitedToOrgId']),
|
|
227
|
+
geo: geoOf(raw['geo']),
|
|
228
|
+
accountCreatedAt: millisOf(raw['accountCreatedAt']),
|
|
229
|
+
recordedAt: typeof raw['recordedAt'] === 'number' ? raw['recordedAt'] : 0,
|
|
230
|
+
recordedBy
|
|
231
|
+
};
|
|
232
|
+
const copiedFrom = uidOf(raw['copiedFromUid']);
|
|
233
|
+
if (copiedFrom) record.copiedFromUid = copiedFrom;
|
|
234
|
+
return record;
|
|
235
|
+
}
|
|
236
|
+
/** A sign-in provider id as a person reads it. */ export function providerLabel(provider) {
|
|
237
|
+
if (!provider) return null;
|
|
238
|
+
if (provider === 'password') return 'password';
|
|
239
|
+
if (provider === 'google.com') return 'Google';
|
|
240
|
+
if (provider === 'apple.com') return 'Apple';
|
|
241
|
+
if (provider === 'microsoft.com') return 'Microsoft';
|
|
242
|
+
if (provider === 'github.com') return 'GitHub';
|
|
243
|
+
if (provider.startsWith('saml.') || provider.startsWith('oidc.')) return 'single sign-on';
|
|
244
|
+
return provider;
|
|
245
|
+
}
|
|
246
|
+
function channelPhrase(record) {
|
|
247
|
+
const from = displayHost(record.referrerHost) || record.source;
|
|
248
|
+
const campaign = record.campaign ? `, ${record.campaign}` : '';
|
|
249
|
+
switch(record.channel){
|
|
250
|
+
case 'referral':
|
|
251
|
+
return `Referral from ${from}${campaign}`;
|
|
252
|
+
case 'organic-search':
|
|
253
|
+
return `Organic search from ${from}`;
|
|
254
|
+
case 'paid-search':
|
|
255
|
+
return `Paid search (${record.source}${campaign})`;
|
|
256
|
+
case 'social':
|
|
257
|
+
return `Social from ${from}${campaign}`;
|
|
258
|
+
case 'email':
|
|
259
|
+
return `Email (${record.source}${campaign})`;
|
|
260
|
+
case 'direct':
|
|
261
|
+
return record.viaHost ? `Direct, first seen arriving from ${record.viaHost}` : 'Direct';
|
|
262
|
+
default:
|
|
263
|
+
return record.recordedBy === 'backfill' ? 'Source unknown — the account predates capture' : 'Source unknown — nothing was captured';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function doorPhrase(record) {
|
|
267
|
+
const provider = providerLabel(record.provider);
|
|
268
|
+
switch(record.door){
|
|
269
|
+
case 'signup-password':
|
|
270
|
+
return 'signed up with password';
|
|
271
|
+
case 'signup-google':
|
|
272
|
+
return 'signed up with Google';
|
|
273
|
+
case 'invite':
|
|
274
|
+
return provider ? `signed up from an invitation, with ${provider}` : 'signed up from an invitation';
|
|
275
|
+
case 'sso':
|
|
276
|
+
return 'signed in with single sign-on';
|
|
277
|
+
default:
|
|
278
|
+
return provider ? `created with ${provider}` : 'created';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The record as one sentence a staff member reads at a glance:
|
|
283
|
+
* "Referral from g2.com → /pricing → signed up with password".
|
|
284
|
+
*
|
|
285
|
+
* The landing is a bare path when it was on `primaryHost` — the platform's own
|
|
286
|
+
* home — and host-and-path anywhere else, so a docs landing says so.
|
|
287
|
+
*/ export function describeAccountAcquisition(record, options = {}) {
|
|
288
|
+
if (!record) return 'No acquisition record';
|
|
289
|
+
const parts = [
|
|
290
|
+
channelPhrase(record)
|
|
291
|
+
];
|
|
292
|
+
if (record.landing) {
|
|
293
|
+
const onPrimary = options.primaryHost && displayHost(record.landing.host) === displayHost(options.primaryHost);
|
|
294
|
+
parts.push(onPrimary ? record.landing.path : `${displayHost(record.landing.host)}${record.landing.path}`);
|
|
295
|
+
}
|
|
296
|
+
parts.push(doorPhrase(record));
|
|
297
|
+
return parts.join(' → ');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
//# sourceMappingURL=account-acquisition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/account-acquisition.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 {\n classifyChannel,\n displayHost,\n sanitizeFirstTouch,\n sourceAndMedium,\n type AcquisitionChannel,\n type FirstTouchClickId,\n type FirstTouchUtm,\n} from '@aglyn/shared-util-first-touch'\n\n/**\n * Where an account came from, written ONCE on the account when it is created\n * (AGL-3289) — `users/{uid}.acquisition`, and on the organization created in\n * the same flow as `orgs/{orgId}.acquisition`.\n *\n * ## Why this exists\n *\n * Answering \"where did this person come from?\" for one new workspace took a\n * sales session twenty-five minutes across five tools: the staff user page\n * had no source, Firestore held nothing beyond names, and the answer — a\n * review site's referral — was finally found in an analytics property and in\n * the marketing host's daily referrer counts. The platform never stored the\n * fact on the account it was about. This record is that fact, read from one\n * card on the staff user and organization pages.\n *\n * ## What writes it, and what never may\n *\n * The capture library keeps the first touch on the visitor's device; the\n * platform turns it into this record at account creation, server-side, and\n * the rules refuse the field to every client — owner and staff alike. A\n * record the account holder could restate would be a record nobody could\n * trust. It is never overwritten: a later touch, if one is ever kept, goes\n * BESIDE it as its own field.\n *\n * Every field here is built from the untrusted touch by re-scrubbing it, the\n * same way the capture scrubbed it on the device, and nothing the record says\n * grants anything: it is a label for a staff reader.\n */\n\n/** The field on `users/{uid}` and `orgs/{orgId}`. */\nexport const ACCOUNT_ACQUISITION_FIELD = 'acquisition'\n\n/**\n * How an account was created: the signup form with a password, the signup\n * form through Google, accepting an invitation, or single sign-on.\n */\nexport const ACQUISITION_DOORS = [\n 'signup-password',\n 'signup-google',\n 'invite',\n 'sso',\n] as const\nexport type AcquisitionDoor = (typeof ACQUISITION_DOORS)[number] | 'unknown'\n\n/** What wrote a record: a door at creation, an organization copying its creator's, or the backfill. */\nexport type AcquisitionRecordedBy = 'signup' | 'sso' | 'org-creation' | 'backfill'\n\n/** Where the account-creating request came from, as the edge reported it. */\nexport interface AcquisitionGeo {\n country: string | null\n region: string | null\n city: string | null\n}\n\n/** The stored record. */\nexport interface AccountAcquisition {\n v: 1\n /**\n * Where the visit came from, spelled the way reports spell it — a\n * `utm_source`, a referring host, `(direct)` — or `unknown` when nothing was\n * captured.\n */\n source: string\n /** `utm_medium`, or what the channel implies; null when unknown. */\n medium: string | null\n /** `utm_campaign`, when the landing URL named one. */\n campaign: string | null\n /** The channel, from the platform's own rule table. */\n channel: AcquisitionChannel\n /** When the first visit landed; null when it was never captured. */\n capturedAt: number | null\n /** The first page the visitor landed on. */\n landing: { host: string; path: string } | null\n /** The external host that sent them. */\n referrerHost: string | null\n /** One of the platform's own hosts they came from before anything captured them. */\n viaHost: string | null\n /** Every `utm_*` the landing URL carried. */\n utm: FirstTouchUtm | null\n /** Which ad click ids the landing URL carried — presence only. */\n clickIds: FirstTouchClickId[]\n /** How the account was created. */\n door: AcquisitionDoor\n /** The sign-in provider it was created with: `password`, `google.com`, `saml.…`. */\n provider: string | null\n /** The organization whose invitation the person signed up to accept. */\n invitedToOrgId: string | null\n /** Where the account-creating request came from. */\n geo: AcquisitionGeo | null\n /** When the auth record says the account was created. */\n accountCreatedAt: number | null\n /** When this record was written. */\n recordedAt: number\n recordedBy: AcquisitionRecordedBy\n /** On an organization's copy: the account it was copied from. */\n copiedFromUid?: string\n}\n\nconst MAX_LABEL = 100\nconst PROVIDER_SHAPE = /^[a-z0-9][a-z0-9._-]{0,63}$/i\nconst RECORDED_BY: readonly AcquisitionRecordedBy[] = [\n 'signup',\n 'sso',\n 'org-creation',\n 'backfill',\n]\n\nfunction label(value: unknown, max = MAX_LABEL): string | null {\n if (typeof value !== 'string') return null\n const clean = value.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim() // eslint-disable-line no-control-regex\n return clean ? clean.slice(0, max) : null\n}\n\nfunction doorOf(value: unknown): AcquisitionDoor {\n return (ACQUISITION_DOORS as readonly string[]).includes(value as string)\n ? (value as AcquisitionDoor)\n : 'unknown'\n}\n\nfunction providerOf(value: unknown): string | null {\n return typeof value === 'string' && PROVIDER_SHAPE.test(value.trim())\n ? value.trim().toLowerCase()\n : null\n}\n\nfunction geoOf(value: unknown): AcquisitionGeo | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const geo: AcquisitionGeo = {\n country: label(raw['country'], 8)?.toUpperCase() ?? null,\n region: label(raw['region'], 64),\n city: label(raw['city'], 64),\n }\n return geo.country || geo.region || geo.city ? geo : null\n}\n\nfunction millisOf(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\nfunction uidOf(value: unknown): string | null {\n return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value) ? value : null\n}\n\n/** What a door knows when it records an account. */\nexport interface AccountAcquisitionInput {\n /** The first touch the capture kept — untrusted, from the device. */\n touch: unknown\n door: unknown\n provider?: unknown\n geo?: unknown\n invitedToOrgId?: unknown\n /** When the auth record says the account was created. */\n accountCreatedAtMs?: number | null\n recordedBy: AcquisitionRecordedBy\n nowMs: number\n}\n\n/**\n * The record for an account being created now.\n *\n * A missing or unusable touch still produces a record — `source: 'unknown'`\n * with whatever the door knows — because \"we asked and nothing came back\" is\n * a fact about the account too, and a card that renders blank would read as\n * \"never asked\".\n */\nexport function buildAccountAcquisition(input: AccountAcquisitionInput): AccountAcquisition {\n const touch = sanitizeFirstTouch(input.touch, input.nowMs)\n const base = {\n door: doorOf(input.door),\n provider: providerOf(input.provider),\n invitedToOrgId: uidOf(input.invitedToOrgId),\n geo: geoOf(input.geo),\n accountCreatedAt: millisOf(input.accountCreatedAtMs),\n recordedAt: input.nowMs,\n recordedBy: input.recordedBy,\n }\n if (!touch) return unknownAccountAcquisition(base)\n const { source, medium } = sourceAndMedium(touch)\n return {\n v: 1,\n source,\n medium,\n campaign: touch.utm?.campaign ?? null,\n channel: classifyChannel(touch),\n capturedAt: touch.at,\n landing: { host: touch.host, path: touch.path },\n referrerHost: touch.ref,\n viaHost: touch.via ?? null,\n utm: touch.utm ?? null,\n clickIds: touch.click ?? [],\n ...base,\n }\n}\n\n/** A record for an account nothing was captured for. */\nexport function unknownAccountAcquisition(input: {\n recordedAt: number\n recordedBy: AcquisitionRecordedBy\n door?: AcquisitionDoor\n provider?: string | null\n invitedToOrgId?: string | null\n geo?: AcquisitionGeo | null\n accountCreatedAt?: number | null\n}): AccountAcquisition {\n return {\n v: 1,\n source: 'unknown',\n medium: null,\n campaign: null,\n channel: 'unknown',\n capturedAt: null,\n landing: null,\n referrerHost: null,\n viaHost: null,\n utm: null,\n clickIds: [],\n door: input.door ?? 'unknown',\n provider: input.provider ?? null,\n invitedToOrgId: input.invitedToOrgId ?? null,\n geo: input.geo ?? null,\n accountCreatedAt: input.accountCreatedAt ?? null,\n recordedAt: input.recordedAt,\n recordedBy: input.recordedBy,\n }\n}\n\n/**\n * The organization's copy of its creator's record, or an `unknown` one naming\n * the creator when the account has none — so an organization's card is never\n * blank either, and always says whose record it is.\n */\nexport function organizationAcquisition(\n creatorRecord: unknown,\n creatorUid: string,\n nowMs: number,\n): AccountAcquisition {\n const record = readAccountAcquisition(creatorRecord)\n const copied = record\n ? { ...record }\n : unknownAccountAcquisition({ recordedAt: nowMs, recordedBy: 'org-creation' })\n delete copied.copiedFromUid\n return { ...copied, copiedFromUid: creatorUid }\n}\n\n/**\n * A stored record as a reader should trust it: re-validated field by field,\n * so a document written by an older version, a backfill or a hand edit still\n * renders, and renders nothing it should not. Null when the value is not a\n * record at all.\n */\nexport function readAccountAcquisition(value: unknown): AccountAcquisition | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const source = label(raw['source'])\n if (!source) return null\n const channel = raw['channel']\n const landing = raw['landing'] as Record<string, unknown> | null\n const utmRaw = raw['utm'] as Record<string, unknown> | null\n const utm: FirstTouchUtm = {}\n for (const key of ['source', 'medium', 'campaign', 'content', 'term'] as const) {\n const cleaned = label(utmRaw?.[key])\n if (cleaned) utm[key] = cleaned\n }\n const clickIds = Array.isArray(raw['clickIds'])\n ? (['gclid', 'fbclid', 'msclkid'] as const).filter((id) =>\n (raw['clickIds'] as unknown[]).includes(id),\n )\n : []\n const recordedBy = RECORDED_BY.includes(raw['recordedBy'] as AcquisitionRecordedBy)\n ? (raw['recordedBy'] as AcquisitionRecordedBy)\n : 'backfill'\n const record: AccountAcquisition = {\n v: 1,\n source,\n medium: label(raw['medium']),\n campaign: label(raw['campaign']),\n channel:\n typeof channel === 'string' &&\n [\n 'organic-search',\n 'paid-search',\n 'social',\n 'referral',\n 'email',\n 'direct',\n 'unknown',\n ].includes(channel)\n ? (channel as AcquisitionChannel)\n : 'unknown',\n capturedAt:\n typeof raw['capturedAt'] === 'number' && raw['capturedAt'] > 0\n ? (raw['capturedAt'] as number)\n : null,\n landing:\n landing && label(landing['host'], 253)\n ? { host: label(landing['host'], 253) as string, path: label(landing['path'], 200) ?? '/' }\n : null,\n referrerHost: label(raw['referrerHost'], 253),\n viaHost: label(raw['viaHost'], 253),\n utm: Object.keys(utm).length ? utm : null,\n clickIds,\n door: doorOf(raw['door']),\n provider: providerOf(raw['provider']),\n invitedToOrgId: uidOf(raw['invitedToOrgId']),\n geo: geoOf(raw['geo']),\n accountCreatedAt: millisOf(raw['accountCreatedAt']),\n recordedAt:\n typeof raw['recordedAt'] === 'number' ? (raw['recordedAt'] as number) : 0,\n recordedBy,\n }\n const copiedFrom = uidOf(raw['copiedFromUid'])\n if (copiedFrom) record.copiedFromUid = copiedFrom\n return record\n}\n\n/** A sign-in provider id as a person reads it. */\nexport function providerLabel(provider: string | null | undefined): string | null {\n if (!provider) return null\n if (provider === 'password') return 'password'\n if (provider === 'google.com') return 'Google'\n if (provider === 'apple.com') return 'Apple'\n if (provider === 'microsoft.com') return 'Microsoft'\n if (provider === 'github.com') return 'GitHub'\n if (provider.startsWith('saml.') || provider.startsWith('oidc.')) return 'single sign-on'\n return provider\n}\n\nfunction channelPhrase(record: AccountAcquisition): string {\n const from = displayHost(record.referrerHost) || record.source\n const campaign = record.campaign ? `, ${record.campaign}` : ''\n switch (record.channel) {\n case 'referral':\n return `Referral from ${from}${campaign}`\n case 'organic-search':\n return `Organic search from ${from}`\n case 'paid-search':\n return `Paid search (${record.source}${campaign})`\n case 'social':\n return `Social from ${from}${campaign}`\n case 'email':\n return `Email (${record.source}${campaign})`\n case 'direct':\n return record.viaHost\n ? `Direct, first seen arriving from ${record.viaHost}`\n : 'Direct'\n default:\n return record.recordedBy === 'backfill'\n ? 'Source unknown — the account predates capture'\n : 'Source unknown — nothing was captured'\n }\n}\n\nfunction doorPhrase(record: AccountAcquisition): string {\n const provider = providerLabel(record.provider)\n switch (record.door) {\n case 'signup-password':\n return 'signed up with password'\n case 'signup-google':\n return 'signed up with Google'\n case 'invite':\n return provider\n ? `signed up from an invitation, with ${provider}`\n : 'signed up from an invitation'\n case 'sso':\n return 'signed in with single sign-on'\n default:\n return provider ? `created with ${provider}` : 'created'\n }\n}\n\n/**\n * The record as one sentence a staff member reads at a glance:\n * \"Referral from g2.com → /pricing → signed up with password\".\n *\n * The landing is a bare path when it was on `primaryHost` — the platform's own\n * home — and host-and-path anywhere else, so a docs landing says so.\n */\nexport function describeAccountAcquisition(\n record: AccountAcquisition | null | undefined,\n options: { primaryHost?: string | null } = {},\n): string {\n if (!record) return 'No acquisition record'\n const parts = [channelPhrase(record)]\n if (record.landing) {\n const onPrimary =\n options.primaryHost &&\n displayHost(record.landing.host) === displayHost(options.primaryHost)\n parts.push(\n onPrimary ? record.landing.path : `${displayHost(record.landing.host)}${record.landing.path}`,\n )\n }\n parts.push(doorPhrase(record))\n return parts.join(' → ')\n}\n"],"names":["classifyChannel","displayHost","sanitizeFirstTouch","sourceAndMedium","ACCOUNT_ACQUISITION_FIELD","ACQUISITION_DOORS","MAX_LABEL","PROVIDER_SHAPE","RECORDED_BY","label","value","max","clean","replace","trim","slice","doorOf","includes","providerOf","test","toLowerCase","geoOf","raw","geo","country","toUpperCase","region","city","millisOf","Number","isFinite","uidOf","buildAccountAcquisition","input","touch","nowMs","base","door","provider","invitedToOrgId","accountCreatedAt","accountCreatedAtMs","recordedAt","recordedBy","unknownAccountAcquisition","source","medium","v","campaign","utm","channel","capturedAt","at","landing","host","path","referrerHost","ref","viaHost","via","clickIds","click","organizationAcquisition","creatorRecord","creatorUid","record","readAccountAcquisition","copied","copiedFromUid","utmRaw","key","cleaned","Array","isArray","filter","id","Object","keys","length","copiedFrom","providerLabel","startsWith","channelPhrase","from","doorPhrase","describeAccountAcquisition","options","parts","onPrimary","primaryHost","push","join"],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
* The hosts this install serves ITSELF — the first-party host registry the
|
|
19
|
+
* first-touch capture reads (AGL-3289).
|
|
20
|
+
*
|
|
21
|
+
* A referrer on one of these is internal and never a first touch, and the
|
|
22
|
+
* capture only runs on a page served from one. Two layers:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Built in**, derived from configuration every install already has: the
|
|
25
|
+
* workspace domain and every subdomain of it (the console, its auth host,
|
|
26
|
+
* each workspace, the docs host under it), plus the configured console,
|
|
27
|
+
* docs and home origins wherever they live. A self-hosted install gets its
|
|
28
|
+
* own list from its own configuration, with nothing new to set.
|
|
29
|
+
* 2. **Configured**, a list staff edit on the Platform settings page and the
|
|
30
|
+
* platform keeps in `platformSettings/firstPartyHosts` — for a surface the
|
|
31
|
+
* configuration cannot name: a forum, a status page, another apex.
|
|
32
|
+
*
|
|
33
|
+
* Adding a surface is therefore: register its host (unless the workspace
|
|
34
|
+
* domain already covers it), and include the capture. Nothing else.
|
|
35
|
+
*
|
|
36
|
+
* ## What is deliberately NOT first-party
|
|
37
|
+
*
|
|
38
|
+
* Published customer sites. A visitor who follows a customer's "made with"
|
|
39
|
+
* badge to the platform arrived from somebody else's site, which is exactly
|
|
40
|
+
* the kind of referral the record is for. The tenant domain lives outside the
|
|
41
|
+
* workspace domain on the platform's own deployment; where an install nests it
|
|
42
|
+
* inside, the registry EXCLUDES it explicitly rather than trusting the
|
|
43
|
+
* wildcard.
|
|
44
|
+
*/
|
|
45
|
+
/** The `platformSettings` document the configured list lives in. */
|
|
46
|
+
export declare const FIRST_PARTY_HOSTS_SETTINGS_DOC = "firstPartyHosts";
|
|
47
|
+
/** How many entries staff may configure; the built-in list is not counted. */
|
|
48
|
+
export declare const FIRST_PARTY_HOSTS_MAX = 50;
|
|
49
|
+
/** The configuration the built-in list is derived from. */
|
|
50
|
+
export interface PlatformHostInputs {
|
|
51
|
+
/** The domain workspaces are served under; its subdomains are all ours. */
|
|
52
|
+
workspaceDomain: string | null;
|
|
53
|
+
consoleUrl: string | null;
|
|
54
|
+
docsUrl: string | null;
|
|
55
|
+
homeUrl: string | null;
|
|
56
|
+
/** The domain customer sites are served under. */
|
|
57
|
+
tenantDomain: string | null;
|
|
58
|
+
/** Local development, where the console and the sites run on localhost. */
|
|
59
|
+
development: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A registry entry as staff typed it, or '' when it is not one: a host, a
|
|
63
|
+
* `*.` wildcard, either with a leading `!` to exclude, or a URL whose host is
|
|
64
|
+
* taken. Every host but `localhost` needs a dot, so nobody can make a whole
|
|
65
|
+
* top-level domain internal by typing `*.com`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function normalizeFirstPartyHostEntry(value: unknown): string;
|
|
68
|
+
/** The built-in list for a set of configuration values. */
|
|
69
|
+
export declare function builtInFirstPartyHosts(inputs: PlatformHostInputs): string[];
|
|
70
|
+
/**
|
|
71
|
+
* This deployment's configuration, read the way the rest of the platform
|
|
72
|
+
* reads it.
|
|
73
|
+
*
|
|
74
|
+
* The workspace domain falls back to the home URL's host when its variable is
|
|
75
|
+
* unset — which it is on the platform's own production console, where the
|
|
76
|
+
* code-level default and the home URL name the same apex. Dot notation
|
|
77
|
+
* throughout, so a browser bundle gets the build-time values.
|
|
78
|
+
*/
|
|
79
|
+
export declare function platformHostInputs(): PlatformHostInputs;
|
|
80
|
+
/**
|
|
81
|
+
* The whole registry: the built-in list, then whatever staff configured,
|
|
82
|
+
* cleaned and de-duplicated. An unusable configured entry is dropped rather
|
|
83
|
+
* than failing the list, because a registry that refused to load would switch
|
|
84
|
+
* the capture off everywhere at once.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mergeFirstPartyHosts(builtIn: readonly string[], configured: unknown): string[];
|