@lowdefy/helpers 6.1.0 → 7.0.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/dist/appStorageKey.js +20 -0
- package/dist/buildPagePath.js +39 -0
- package/dist/builtinMessages.js +7 -0
- package/dist/callerHash.js +28 -0
- package/dist/callerStamp.js +59 -0
- package/dist/cleanBuildArtifact.js +24 -0
- package/dist/createTraceId.js +35 -0
- package/dist/filterElementsChain.js +68 -0
- package/dist/findInteractiveControls.js +25 -0
- package/dist/getSafeHref.js +49 -0
- package/dist/index.js +24 -1
- package/dist/isChainHeaderEnd.js +33 -0
- package/dist/isInteractiveControl.js +22 -0
- package/dist/isMountEventName.js +27 -0
- package/dist/isTraceId.js +21 -0
- package/dist/journeyTargetSelectors.js +103 -0
- package/dist/listHash.js +25 -0
- package/dist/normalizeCaller.js +30 -0
- package/dist/pageInstanceKey.js +36 -0
- package/dist/pairTraceEvents.js +102 -0
- package/dist/parseCallerStamp.js +46 -0
- package/dist/parseElementsChain.js +58 -0
- package/dist/parsePageId.js +23 -0
- package/dist/parsePathPattern.js +88 -0
- package/dist/parseRowIndex.js +24 -0
- package/dist/projectCaughtError.js +6 -2
- package/dist/readChainValue.js +42 -0
- package/dist/rolesHash.js +34 -0
- package/dist/serializer.js +3 -2
- package/dist/sha256Hex.js +170 -0
- package/dist/stampHash.js +24 -0
- package/dist/targetFromElementsChain.js +103 -0
- package/dist/traceIdDate.js +23 -0
- package/package.json +5 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // Browser storage is shared by every app on an origin, so each key Lowdefy writes names the app
|
|
16
|
+
// it belongs to: its slug and the base path it is served under.
|
|
17
|
+
function appStorageKey({ basePath, name, slug }) {
|
|
18
|
+
return `lowdefy:${slug ?? ''}:${basePath ?? ''}:${name}`;
|
|
19
|
+
}
|
|
20
|
+
export default appStorageKey;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import parsePathPattern from './parsePathPattern.js';
|
|
16
|
+
import type from './type.js';
|
|
17
|
+
// The URL path of a page, with no leading "/" and no basePath.
|
|
18
|
+
function buildPagePath({ pageId, path, pathParams }) {
|
|
19
|
+
if (type.isUndefined(path)) {
|
|
20
|
+
return pageId;
|
|
21
|
+
}
|
|
22
|
+
const values = pathParams ?? {};
|
|
23
|
+
return parsePathPattern(path).map((segment)=>{
|
|
24
|
+
if (type.isString(segment.fixed)) {
|
|
25
|
+
return segment.fixed;
|
|
26
|
+
}
|
|
27
|
+
const value = values[segment.name];
|
|
28
|
+
if (type.isNone(value) || value === '') {
|
|
29
|
+
throw new Error(`Link to page "${pageId}" is missing a value for path placeholder "${segment.name}".`);
|
|
30
|
+
}
|
|
31
|
+
const text = String(value);
|
|
32
|
+
// The URL parser removes a "." or ".." segment, in any encoding, before the request is sent.
|
|
33
|
+
if (text === '.' || text === '..') {
|
|
34
|
+
throw new Error(`Link to page "${pageId}" has the value "${text}" for path placeholder "${segment.name}". A path value cannot be "." or "..".`);
|
|
35
|
+
}
|
|
36
|
+
return encodeURIComponent(text);
|
|
37
|
+
}).join('/');
|
|
38
|
+
}
|
|
39
|
+
export default buildPagePath;
|
package/dist/builtinMessages.js
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
'client.requestError': 'Request error',
|
|
22
22
|
'client.backAriaLabel': 'back',
|
|
23
23
|
'client.shortcutThen': 'then',
|
|
24
|
+
'client.copy': 'Copy',
|
|
25
|
+
'client.copyValue': 'Copy: {value}',
|
|
26
|
+
'client.copied': 'Copied',
|
|
27
|
+
'client.copyFailed': 'Copy failed',
|
|
28
|
+
'client.confirm': 'Are you sure?',
|
|
24
29
|
'errorPage.name': 'Error',
|
|
25
30
|
'errorPage.message': 'An error has occurred.',
|
|
26
31
|
'errorPage.homeLink': 'Return to home page',
|
|
@@ -30,7 +35,9 @@
|
|
|
30
35
|
'agent.runtime.invalidPath': 'Invalid agent path',
|
|
31
36
|
'agent.runtime.messagesMustBeArray': 'messages must be an array',
|
|
32
37
|
'agent.runtime.urlQueryMustBeObject': 'urlQuery must be an object',
|
|
38
|
+
'agent.runtime.pathParamsMustBeObject': 'pathParams must be an object',
|
|
33
39
|
'agent.runtime.sharedStateMustBeObject': 'sharedState must be an object',
|
|
40
|
+
'agent.runtime.sharedStateReadOnlyMustBeBoolean': 'sharedStateReadOnly must be a boolean',
|
|
34
41
|
'agent.runtime.agentNotFound': 'Agent "{agentId}" does not exist.',
|
|
35
42
|
'agent.runtime.agentTypeNotFound': 'Agent type "{type}" can not be found.',
|
|
36
43
|
'agent.runtime.toolExecutionFailed': 'Endpoint execution failed',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import stampHash from './stampHash.js';
|
|
16
|
+
import type from './type.js';
|
|
17
|
+
// A short fingerprint of the caller a response was answered as: the user id
|
|
18
|
+
// and active organization together, so a switched organization reads as
|
|
19
|
+
// another caller. Empty for an anonymous caller. It is the first part of the
|
|
20
|
+
// caller stamp (see callerStamp), which the client compares with the caller it
|
|
21
|
+
// renders.
|
|
22
|
+
function callerHash({ user }) {
|
|
23
|
+
if (type.isNone(user)) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
return stampHash(`${user.id}:${user.organization_id ?? ''}`);
|
|
27
|
+
}
|
|
28
|
+
export default callerHash;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import callerHash from './callerHash.js';
|
|
16
|
+
import listHash from './listHash.js';
|
|
17
|
+
import rolesHash from './rolesHash.js';
|
|
18
|
+
import stampHash from './stampHash.js';
|
|
19
|
+
import type from './type.js';
|
|
20
|
+
// The x-lowdefy-caller header value: who the server answered a page, request
|
|
21
|
+
// or endpoint call as, in full.
|
|
22
|
+
//
|
|
23
|
+
// <caller>; session=<hash>; roles=<hash>; memberships=<hash>
|
|
24
|
+
//
|
|
25
|
+
// caller is callerHash (user and active organization), session the hash of the
|
|
26
|
+
// session id, roles the hash of the caller's app roles and organization role
|
|
27
|
+
// tier in the active organization (rolesHash), memberships the hash of the ids
|
|
28
|
+
// of every organization the person is a member of (under a pinned app, of this
|
|
29
|
+
// app's organization alone - see resolveAuthentication). An anonymous caller is
|
|
30
|
+
// the empty string with no fields. A field the server does not know is left
|
|
31
|
+
// out: session for a caller with no session (an API strategy, an injected dev
|
|
32
|
+
// caller), memberships when the server did not read the person's member rows.
|
|
33
|
+
// parseCallerStamp reads it back.
|
|
34
|
+
//
|
|
35
|
+
// authSession is the session the caller was resolved from, as
|
|
36
|
+
// resolveAuthentication records it: { id, memberOrganizationIds }.
|
|
37
|
+
function callerStamp({ user, authSession }) {
|
|
38
|
+
if (type.isNone(user)) {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
const parts = [
|
|
42
|
+
callerHash({
|
|
43
|
+
user
|
|
44
|
+
})
|
|
45
|
+
];
|
|
46
|
+
if (!type.isNone(authSession)) {
|
|
47
|
+
parts.push(`session=${stampHash(authSession.id)}`);
|
|
48
|
+
}
|
|
49
|
+
parts.push(`roles=${rolesHash({
|
|
50
|
+
user
|
|
51
|
+
})}`);
|
|
52
|
+
if (!type.isNone(authSession?.memberOrganizationIds)) {
|
|
53
|
+
parts.push(`memberships=${listHash({
|
|
54
|
+
values: authSession.memberOrganizationIds
|
|
55
|
+
})}`);
|
|
56
|
+
}
|
|
57
|
+
return parts.join('; ');
|
|
58
|
+
}
|
|
59
|
+
export default callerStamp;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import serializer from './serializer.js';
|
|
16
|
+
// Build artifacts carry serializer markers (~k, ~r, ~l) and wrap location-marked
|
|
17
|
+
// arrays as { '~arr': [...], '~k': '...' }. serializer.deserialize un-wraps
|
|
18
|
+
// ~arr back to a plain array and demotes markers to non-enumerable properties;
|
|
19
|
+
// the subsequent JSON round-trip drops those non-enumerable markers, leaving a
|
|
20
|
+
// plain structure (e.g. a payloadSchema as plain JSON Schema).
|
|
21
|
+
function cleanBuildArtifact(obj) {
|
|
22
|
+
return JSON.parse(JSON.stringify(serializer.deserialize(obj)));
|
|
23
|
+
}
|
|
24
|
+
export default cleanBuildArtifact;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ const SUFFIX_LENGTH = 6;
|
|
16
|
+
function pad(number, length = 2) {
|
|
17
|
+
return String(number).padStart(length, '0');
|
|
18
|
+
}
|
|
19
|
+
function randomSuffix() {
|
|
20
|
+
let suffix = '';
|
|
21
|
+
while(suffix.length < SUFFIX_LENGTH){
|
|
22
|
+
suffix += Math.floor(Math.random() * 36).toString(36);
|
|
23
|
+
}
|
|
24
|
+
return suffix;
|
|
25
|
+
}
|
|
26
|
+
// `<yyyymmdd>T<hhmmss>Z-<6 base36>` in UTC. Names a recorded tab session or a
|
|
27
|
+
// test run; ids sort by time, and the date part picks the trace's date
|
|
28
|
+
// directory, so a session that crosses midnight stays in one file.
|
|
29
|
+
function createTraceId({ now } = {}) {
|
|
30
|
+
const date = new Date(now ?? Date.now());
|
|
31
|
+
const day = `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}`;
|
|
32
|
+
const time = `${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;
|
|
33
|
+
return `${day}T${time}Z-${randomSuffix()}`;
|
|
34
|
+
}
|
|
35
|
+
export default createTraceId;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import isChainHeaderEnd from './isChainHeaderEnd.js';
|
|
16
|
+
import readChainValue from './readChainValue.js';
|
|
17
|
+
import type from './type.js';
|
|
18
|
+
// posthog-js escapes a quote in a value as \", and an existing \" the same way.
|
|
19
|
+
function escapeValue(value) {
|
|
20
|
+
return value.replace(/"|\\"/g, '\\"');
|
|
21
|
+
}
|
|
22
|
+
// Rewrites the attributes of a posthog-js `$elements_chain`, read the way parseElementsChain
|
|
23
|
+
// reads them. filterAttribute({ key, value }) is called for every `key="value"` pair (the
|
|
24
|
+
// `text=` entries included) and returns the value to keep (the same string keeps the pair byte
|
|
25
|
+
// for byte, another string replaces its value), or null to drop the pair. Tags, classes and
|
|
26
|
+
// separators are left as they were. A malformed tail that parseElementsChain would stop at is
|
|
27
|
+
// dropped, so nothing unread is sent on.
|
|
28
|
+
function filterElementsChain({ chain, filterAttribute }) {
|
|
29
|
+
let output = '';
|
|
30
|
+
let index = 0;
|
|
31
|
+
while(index < chain.length){
|
|
32
|
+
let headerEnd = index;
|
|
33
|
+
while(headerEnd < chain.length && chain[headerEnd] !== ';' && !isChainHeaderEnd(chain, headerEnd)){
|
|
34
|
+
headerEnd += 1;
|
|
35
|
+
}
|
|
36
|
+
output += chain.slice(index, headerEnd);
|
|
37
|
+
if (chain[headerEnd] === ':') {
|
|
38
|
+
output += ':';
|
|
39
|
+
index = headerEnd + 1;
|
|
40
|
+
while(index < chain.length && chain[index] !== ';'){
|
|
41
|
+
const keyEnd = chain.indexOf('="', index);
|
|
42
|
+
if (keyEnd === -1) {
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
const key = chain.slice(index, keyEnd);
|
|
46
|
+
const { value, end } = readChainValue(chain, keyEnd + 2);
|
|
47
|
+
const kept = filterAttribute({
|
|
48
|
+
key,
|
|
49
|
+
value
|
|
50
|
+
});
|
|
51
|
+
if (kept === value) {
|
|
52
|
+
output += chain.slice(index, end);
|
|
53
|
+
} else if (type.isString(kept)) {
|
|
54
|
+
output += `${key}="${escapeValue(kept)}"`;
|
|
55
|
+
}
|
|
56
|
+
index = end;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
index = headerEnd;
|
|
60
|
+
}
|
|
61
|
+
if (index < chain.length) {
|
|
62
|
+
output += ';';
|
|
63
|
+
}
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
export default filterElementsChain;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import isInteractiveControl from './isInteractiveControl.js';
|
|
16
|
+
import journeyTargetSelectors from './journeyTargetSelectors.js';
|
|
17
|
+
const candidates = journeyTargetSelectors.interactiveControlRules.map(({ selector })=>selector).join(', ');
|
|
18
|
+
// The interactive controls inside root, in document order, as
|
|
19
|
+
// root.querySelectorAll(journeyTargetSelectors.interactiveControl) finds them.
|
|
20
|
+
function findInteractiveControls(root) {
|
|
21
|
+
return [
|
|
22
|
+
...root.querySelectorAll(candidates)
|
|
23
|
+
].filter(isInteractiveControl);
|
|
24
|
+
}
|
|
25
|
+
export default findInteractiveControls;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import type from './type.js';
|
|
16
|
+
// DOMPurify's default allowed URI schemes, so an href set as a block property
|
|
17
|
+
// links exactly when the same URL would survive in sanitised HTML.
|
|
18
|
+
const safeSchemes = new Set([
|
|
19
|
+
'callto',
|
|
20
|
+
'cid',
|
|
21
|
+
'ftp',
|
|
22
|
+
'ftps',
|
|
23
|
+
'http',
|
|
24
|
+
'https',
|
|
25
|
+
'mailto',
|
|
26
|
+
'matrix',
|
|
27
|
+
'sms',
|
|
28
|
+
'tel',
|
|
29
|
+
'xmpp'
|
|
30
|
+
]);
|
|
31
|
+
const schemePattern = /^([a-z][a-z0-9+.-]*):/i;
|
|
32
|
+
// The browser's URL parser drops leading spaces and controls, and tabs and
|
|
33
|
+
// newlines anywhere, before it reads the scheme. A value it would read
|
|
34
|
+
// differently from how it is written is refused rather than normalised.
|
|
35
|
+
const leadingSpacePattern = /^\s/;
|
|
36
|
+
// eslint-disable-next-line no-control-regex
|
|
37
|
+
const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
|
|
38
|
+
// The href a block renders, or undefined when the value must not become a link:
|
|
39
|
+
// URLs with a scheme DOMPurify allows by default pass, as do relative paths,
|
|
40
|
+
// query-only and fragment URLs. Any other scheme renders the element without an href.
|
|
41
|
+
function getSafeHref(href) {
|
|
42
|
+
if (!type.isString(href) || href === '') return undefined;
|
|
43
|
+
if (leadingSpacePattern.test(href) || controlCharacterPattern.test(href)) return undefined;
|
|
44
|
+
const scheme = schemePattern.exec(href);
|
|
45
|
+
if (scheme === null) return href;
|
|
46
|
+
if (safeSchemes.has(scheme[1].toLowerCase())) return href;
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
export default getSafeHref;
|
package/dist/index.js
CHANGED
|
@@ -13,32 +13,55 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import applyArrayIndices from './applyArrayIndices.js';
|
|
16
|
+
import appStorageKey from './appStorageKey.js';
|
|
16
17
|
import builtinMessages from './builtinMessages.js';
|
|
18
|
+
import buildPagePath from './buildPagePath.js';
|
|
17
19
|
import cachedPromises from './cachedPromises.js';
|
|
20
|
+
import callerHash from './callerHash.js';
|
|
21
|
+
import callerStamp from './callerStamp.js';
|
|
22
|
+
import cleanBuildArtifact from './cleanBuildArtifact.js';
|
|
23
|
+
import createTraceId from './createTraceId.js';
|
|
18
24
|
import extractErrorProps from './extractErrorProps.js';
|
|
25
|
+
import filterElementsChain from './filterElementsChain.js';
|
|
26
|
+
import findInteractiveControls from './findInteractiveControls.js';
|
|
19
27
|
import get from './get.js';
|
|
20
28
|
import getKey from './getKey.js';
|
|
21
29
|
import getLocaleDateFormat from './getLocaleDateFormat.js';
|
|
22
30
|
import getLocaleDecimalSeparator from './getLocaleDecimalSeparator.js';
|
|
23
31
|
import getLocaleGroupSeparator from './getLocaleGroupSeparator.js';
|
|
24
32
|
import getOperatorType from './getOperatorType.js';
|
|
33
|
+
import getSafeHref from './getSafeHref.js';
|
|
34
|
+
import isInteractiveControl from './isInteractiveControl.js';
|
|
35
|
+
import isMountEventName from './isMountEventName.js';
|
|
36
|
+
import isTraceId from './isTraceId.js';
|
|
25
37
|
import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
|
|
26
38
|
import joinPath from './joinPath.js';
|
|
39
|
+
import journeyTargetSelectors from './journeyTargetSelectors.js';
|
|
27
40
|
import LRUCache from './LRUCache.js';
|
|
28
41
|
import mapPlainValues from './mapPlainValues.js';
|
|
29
42
|
import mergeObjects from './mergeObjects.js';
|
|
43
|
+
import normalizeCaller from './normalizeCaller.js';
|
|
30
44
|
import omit from './omit.js';
|
|
45
|
+
import pageInstanceKey from './pageInstanceKey.js';
|
|
46
|
+
import pairTraceEvents from './pairTraceEvents.js';
|
|
47
|
+
import parseCallerStamp from './parseCallerStamp.js';
|
|
48
|
+
import parsePageId from './parsePageId.js';
|
|
49
|
+
import parsePathPattern from './parsePathPattern.js';
|
|
50
|
+
import parseRowIndex from './parseRowIndex.js';
|
|
31
51
|
import projectCaughtError from './projectCaughtError.js';
|
|
52
|
+
import rolesHash from './rolesHash.js';
|
|
32
53
|
import serializer from './serializer.js';
|
|
33
54
|
import set from './set.js';
|
|
34
55
|
import setKey from './setKey.js';
|
|
35
56
|
import splitPath from './splitPath.js';
|
|
36
57
|
import stableStringify from './stableStringify.js';
|
|
37
58
|
import swap from './swap.js';
|
|
59
|
+
import traceIdDate from './traceIdDate.js';
|
|
60
|
+
import targetFromElementsChain from './targetFromElementsChain.js';
|
|
38
61
|
import translate from './translate.js';
|
|
39
62
|
import type from './type.js';
|
|
40
63
|
import unset from './unset.js';
|
|
41
64
|
import unsetKey from './unsetKey.js';
|
|
42
65
|
import urlQuery from './urlQuery.js';
|
|
43
66
|
import wait from './wait.js';
|
|
44
|
-
export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getKey, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, getOperatorType, isReserved, joinPath, LRUCache, mapPlainValues, mergeObjects, omit, projectCaughtError, ReservedKeyError, serializer, set, setKey, splitPath, stableStringify, swap, translate, type, unset, unsetKey, urlQuery, wait };
|
|
67
|
+
export { applyArrayIndices, appStorageKey, buildPagePath, builtinMessages, cachedPromises, callerHash, callerStamp, cleanBuildArtifact, createTraceId, extractErrorProps, filterElementsChain, findInteractiveControls, get, getKey, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, getOperatorType, getSafeHref, isInteractiveControl, isMountEventName, isTraceId, isReserved, joinPath, journeyTargetSelectors, LRUCache, mapPlainValues, mergeObjects, normalizeCaller, omit, pageInstanceKey, pairTraceEvents, parseCallerStamp, parsePageId, parsePathPattern, parseRowIndex, projectCaughtError, ReservedKeyError, rolesHash, serializer, set, setKey, splitPath, stableStringify, swap, traceIdDate, targetFromElementsChain, translate, type, unset, unsetKey, urlQuery, wait };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // Every entry's attributes start with one of these keys, since posthog-js sorts them and always
|
|
16
|
+
// writes nth-child: the colon before one ends the tag and class part, which can itself hold a
|
|
17
|
+
// colon (a Tailwind class like `md:flex`).
|
|
18
|
+
const FIRST_ATTRIBUTE_PREFIXES = [
|
|
19
|
+
'attr_',
|
|
20
|
+
'href="',
|
|
21
|
+
'nth-child="'
|
|
22
|
+
];
|
|
23
|
+
function isChainHeaderEnd(chain, index) {
|
|
24
|
+
if (chain[index] !== ':') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const next = index + 1;
|
|
28
|
+
if (next === chain.length || chain[next] === ';') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return FIRST_ATTRIBUTE_PREFIXES.some((prefix)=>chain.startsWith(prefix, next));
|
|
32
|
+
}
|
|
33
|
+
export default isChainHeaderEnd;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import journeyTargetSelectors from './journeyTargetSelectors.js';
|
|
16
|
+
// Whether an element is an interactive control in the journey runner's sense: it matches
|
|
17
|
+
// journeyTargetSelectors.interactiveControl. Reads the rules one by one, so it also works in DOMs
|
|
18
|
+
// that cannot parse the combined selector (jsdom).
|
|
19
|
+
function isInteractiveControl(element) {
|
|
20
|
+
return journeyTargetSelectors.interactiveControlRules.some(({ selector, has, not = [] })=>element.matches(selector) && (!has || element.querySelector(has) !== null) && !not.some((excluded)=>element.matches(excluded)));
|
|
21
|
+
}
|
|
22
|
+
export default isInteractiveControl;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ const MOUNT_EVENT_NAMES = [
|
|
16
|
+
'onInit',
|
|
17
|
+
'onInitAsync',
|
|
18
|
+
'onMount',
|
|
19
|
+
'onMountAsync'
|
|
20
|
+
];
|
|
21
|
+
// Mount-class events fire by themselves when a page or block opens, so no DOM
|
|
22
|
+
// interaction ever causes one. The pairing rule and the journey compiler both
|
|
23
|
+
// need to tell them apart, and must agree on the list.
|
|
24
|
+
function isMountEventName({ eventName }) {
|
|
25
|
+
return MOUNT_EVENT_NAMES.includes(eventName);
|
|
26
|
+
}
|
|
27
|
+
export default isMountEventName;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ const TRACE_ID_PATTERN = /^\d{8}T\d{6}Z-[a-z0-9]{6}$/;
|
|
16
|
+
// A trace id names a session or run file on disk, so this check is also the
|
|
17
|
+
// path-traversal guard for anything that builds a path from one.
|
|
18
|
+
function isTraceId(value) {
|
|
19
|
+
return typeof value === 'string' && TRACE_ID_PATTERN.test(value);
|
|
20
|
+
}
|
|
21
|
+
export default isTraceId;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // The rules a journey target is resolved by. The journey runner resolves targets with them, and
|
|
16
|
+
// the engine's describe functions read the same rules in reverse, so the two cannot drift apart.
|
|
17
|
+
// A radio or checkbox inside a <label> is reached through the label: the label carries the
|
|
18
|
+
// option's text and is what a person clicks, while the input may have no size at all (antd
|
|
19
|
+
// Segmented hides it at zero width and height).
|
|
20
|
+
const radioOptionRule = {
|
|
21
|
+
selector: 'label',
|
|
22
|
+
has: 'input[type="radio"]'
|
|
23
|
+
};
|
|
24
|
+
const checkboxOptionRule = {
|
|
25
|
+
selector: 'label',
|
|
26
|
+
has: 'input[type="checkbox"]'
|
|
27
|
+
};
|
|
28
|
+
function selectorOf({ selector, has, not = [] }) {
|
|
29
|
+
const hasPart = has ? `:has(${has})` : '';
|
|
30
|
+
return `${selector}${hasPart}${not.map((item)=>`:not(${item})`).join('')}`;
|
|
31
|
+
}
|
|
32
|
+
const radioOption = selectorOf(radioOptionRule);
|
|
33
|
+
const checkboxOption = selectorOf(checkboxOptionRule);
|
|
34
|
+
// ARIA roles that make an element an interactive control.
|
|
35
|
+
const interactiveRoles = [
|
|
36
|
+
'button',
|
|
37
|
+
'switch',
|
|
38
|
+
'checkbox',
|
|
39
|
+
'radio',
|
|
40
|
+
'tab',
|
|
41
|
+
'menuitem'
|
|
42
|
+
];
|
|
43
|
+
// The controls a click target resolves to. Each rule is a simple selector, a descendant it must
|
|
44
|
+
// have and selectors it must not match, so the rules serve both as one selector string
|
|
45
|
+
// (Playwright, browsers) and as a matcher (isInteractiveControl) for DOMs whose selector engine
|
|
46
|
+
// gets :has() and complex :not() wrong, such as jsdom.
|
|
47
|
+
const interactiveControlRules = [
|
|
48
|
+
{
|
|
49
|
+
selector: 'button'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
selector: '[role="button"]'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
selector: 'a[href]'
|
|
56
|
+
},
|
|
57
|
+
radioOptionRule,
|
|
58
|
+
checkboxOptionRule,
|
|
59
|
+
{
|
|
60
|
+
selector: 'input:not([type="hidden"])',
|
|
61
|
+
not: [
|
|
62
|
+
'label input[type="radio"]',
|
|
63
|
+
'label input[type="checkbox"]'
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
selector: 'textarea'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
selector: 'select'
|
|
71
|
+
},
|
|
72
|
+
...interactiveRoles.filter((role)=>role !== 'button').map((role)=>({
|
|
73
|
+
selector: `[role="${role}"]`
|
|
74
|
+
}))
|
|
75
|
+
];
|
|
76
|
+
// A block that renders into a portal (Modal, Drawer) puts this class on the portal's root, so a
|
|
77
|
+
// journey target scoped to the block reaches the portal content, which is outside the block's
|
|
78
|
+
// #bl- wrapper. The class is a CSS selector-safe form of the block id.
|
|
79
|
+
function portalClass(blockId) {
|
|
80
|
+
return `ldf-portal-${String(blockId).replace(/[^\w-]/g, '_')}`;
|
|
81
|
+
}
|
|
82
|
+
const journeyTargetSelectors = {
|
|
83
|
+
// Every block renders a wrapper with id `bl-<blockId>`.
|
|
84
|
+
blockWrapperPrefix: 'bl-',
|
|
85
|
+
// ag-grid's displayed row index and a cell's column id.
|
|
86
|
+
cellAttribute: 'col-id',
|
|
87
|
+
checkboxOption,
|
|
88
|
+
// The options of a dropdown, the elements a `select` step clicks.
|
|
89
|
+
dropdownOption: '.ant-select-item-option, [role="option"]',
|
|
90
|
+
interactiveControl: interactiveControlRules.map(selectorOf).join(', '),
|
|
91
|
+
interactiveControlRules,
|
|
92
|
+
interactiveRoles,
|
|
93
|
+
// Portal layers, front-most first: an open dropdown menu covers a dialog, a dialog covers the
|
|
94
|
+
// page.
|
|
95
|
+
layers: [
|
|
96
|
+
'[role="menu"]',
|
|
97
|
+
'[role="dialog"]'
|
|
98
|
+
],
|
|
99
|
+
portalClass,
|
|
100
|
+
radioOption,
|
|
101
|
+
rowAttribute: 'row-index'
|
|
102
|
+
};
|
|
103
|
+
export default journeyTargetSelectors;
|