@lowdefy/helpers 6.0.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 +8 -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/extractErrorProps.js +64 -58
- package/dist/filterElementsChain.js +68 -0
- package/dist/findInteractiveControls.js +25 -0
- package/dist/getSafeHref.js +49 -0
- package/dist/index.js +26 -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/mapPlainValues.js +51 -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 +88 -0
- package/dist/readChainValue.js +42 -0
- package/dist/rolesHash.js +34 -0
- package/dist/serializer.js +10 -22
- 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,15 +21,23 @@
|
|
|
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',
|
|
32
|
+
'server.genericError': 'Something went wrong.',
|
|
27
33
|
// Agent runtime — surfaces in browser as HTTP error responses / streamed errors
|
|
28
34
|
'agent.runtime.methodNotAllowed': 'Only POST requests are supported.',
|
|
29
35
|
'agent.runtime.invalidPath': 'Invalid agent path',
|
|
30
36
|
'agent.runtime.messagesMustBeArray': 'messages must be an array',
|
|
31
37
|
'agent.runtime.urlQueryMustBeObject': 'urlQuery must be an object',
|
|
38
|
+
'agent.runtime.pathParamsMustBeObject': 'pathParams must be an object',
|
|
32
39
|
'agent.runtime.sharedStateMustBeObject': 'sharedState must be an object',
|
|
40
|
+
'agent.runtime.sharedStateReadOnlyMustBeBoolean': 'sharedStateReadOnly must be a boolean',
|
|
33
41
|
'agent.runtime.agentNotFound': 'Agent "{agentId}" does not exist.',
|
|
34
42
|
'agent.runtime.agentTypeNotFound': 'Agent type "{type}" can not be found.',
|
|
35
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;
|
|
@@ -46,8 +46,22 @@ function symbolMarker(val) {
|
|
|
46
46
|
}
|
|
47
47
|
const MAX_CAUSE_DEPTH = 3;
|
|
48
48
|
const MAX_OBJECT_DEPTH = 5;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Today's fields in today's order. The own `cause` key is skipped in the spread
|
|
50
|
+
// so an assigned (and therefore enumerable) cause is not listed twice.
|
|
51
|
+
function defaultProjection(err) {
|
|
52
|
+
const props = {
|
|
53
|
+
message: err.message,
|
|
54
|
+
name: err.name,
|
|
55
|
+
stack: err.stack,
|
|
56
|
+
cause: err.cause
|
|
57
|
+
};
|
|
58
|
+
for (const key of Object.keys(err)){
|
|
59
|
+
if (key === 'cause') continue;
|
|
60
|
+
props[key] = err[key];
|
|
61
|
+
}
|
|
62
|
+
return props;
|
|
63
|
+
}
|
|
64
|
+
function cleanValue(val, seen, objectDepth, causeDepth, project) {
|
|
51
65
|
if (objectDepth > MAX_OBJECT_DEPTH) return '[Truncated]';
|
|
52
66
|
if (typeof val === 'function') return functionMarker(val);
|
|
53
67
|
if (typeof val === 'bigint') return bigintMarker(val);
|
|
@@ -56,81 +70,73 @@ function cleanValue(val, seen, objectDepth, causeDepth, omit) {
|
|
|
56
70
|
if (seen.has(val)) return '[Circular]';
|
|
57
71
|
seen.add(val);
|
|
58
72
|
if (Array.isArray(val)) {
|
|
59
|
-
return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth,
|
|
73
|
+
return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth, project));
|
|
60
74
|
}
|
|
61
75
|
if (val instanceof Date) return val;
|
|
62
|
-
if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth,
|
|
76
|
+
if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth, project);
|
|
63
77
|
if (!isPlainObject(val)) return classInstanceMarker(val);
|
|
64
78
|
const cleaned = {};
|
|
65
79
|
for (const [k, v] of Object.entries(val)){
|
|
66
|
-
const cv = cleanValue(v, seen, objectDepth + 1, causeDepth,
|
|
80
|
+
const cv = cleanValue(v, seen, objectDepth + 1, causeDepth, project);
|
|
67
81
|
if (cv !== undefined) cleaned[k] = cv;
|
|
68
82
|
}
|
|
69
83
|
return cleaned;
|
|
70
84
|
}
|
|
71
|
-
function
|
|
85
|
+
function cleanCause(cause, seen, objectDepth, causeDepth, project) {
|
|
86
|
+
if (!(cause instanceof Error)) {
|
|
87
|
+
return cleanValue(cause, seen, objectDepth + 1, causeDepth, project);
|
|
88
|
+
}
|
|
89
|
+
if (seen.has(cause)) return '[Circular]';
|
|
90
|
+
if (causeDepth >= MAX_CAUSE_DEPTH) return '[Truncated]';
|
|
91
|
+
return _extractErrorProps(cause, seen, objectDepth, causeDepth + 1, project);
|
|
92
|
+
}
|
|
93
|
+
function cleanPropValue(value, seen, objectDepth, causeDepth, project) {
|
|
94
|
+
if (typeof value === 'function') return functionMarker(value);
|
|
95
|
+
if (typeof value === 'bigint') return bigintMarker(value);
|
|
96
|
+
if (typeof value === 'symbol') return symbolMarker(value);
|
|
97
|
+
if (value === null || typeof value !== 'object') return value;
|
|
98
|
+
if (value instanceof Date) return value;
|
|
99
|
+
if (Array.isArray(value)) return cleanValue(value, seen, objectDepth + 1, causeDepth, project);
|
|
100
|
+
if (value instanceof Error) {
|
|
101
|
+
if (seen.has(value)) return '[Circular]';
|
|
102
|
+
return _extractErrorProps(value, seen, objectDepth, causeDepth, project);
|
|
103
|
+
}
|
|
104
|
+
if (isPlainObject(value)) return cleanValue(value, seen, objectDepth + 1, causeDepth, project);
|
|
105
|
+
// A class instance (Socket, Agent, ClientRequest, ...). Its internals are never exposed —
|
|
106
|
+
// only the type name — but the key must not vanish: an absent key is indistinguishable
|
|
107
|
+
// from "no such field", and this output is now read by `get` as app-visible config data.
|
|
108
|
+
return classInstanceMarker(value);
|
|
109
|
+
}
|
|
110
|
+
function _extractErrorProps(err, seen, objectDepth, causeDepth, project) {
|
|
72
111
|
if (!err) return err;
|
|
73
112
|
seen.add(err);
|
|
74
113
|
// Called at every error node rather than once for the walk, so a caller's
|
|
75
114
|
// policy can key on the node itself - its class, or its own cause value.
|
|
76
115
|
// Which fields an audience may see is the caller's decision, never this
|
|
77
|
-
// walk's; this function only
|
|
78
|
-
//
|
|
79
|
-
// and never marked in `seen`.
|
|
80
|
-
const
|
|
116
|
+
// walk's; this function only cleans the entries it is handed. An entry the
|
|
117
|
+
// projection leaves out is never built, so a dropped cause is never
|
|
118
|
+
// deep-copied and never marked in `seen`.
|
|
119
|
+
const projected = (project ?? defaultProjection)(err);
|
|
81
120
|
const props = {};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!(err.cause instanceof Error)) {
|
|
87
|
-
props.cause = cleanValue(err.cause, seen, objectDepth + 1, causeDepth, omit);
|
|
88
|
-
} else if (seen.has(err.cause)) {
|
|
89
|
-
props.cause = '[Circular]';
|
|
90
|
-
} else if (causeDepth >= MAX_CAUSE_DEPTH) {
|
|
91
|
-
props.cause = '[Truncated]';
|
|
92
|
-
} else {
|
|
93
|
-
props.cause = _extractErrorProps(err.cause, seen, objectDepth, causeDepth + 1, omit);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
for (const key of Object.keys(err)){
|
|
97
|
-
if (key === 'cause' || omitted.includes(key)) continue;
|
|
98
|
-
const value = err[key];
|
|
99
|
-
if (typeof value === 'function') {
|
|
100
|
-
props[key] = functionMarker(value);
|
|
101
|
-
} else if (typeof value === 'bigint') {
|
|
102
|
-
props[key] = bigintMarker(value);
|
|
103
|
-
} else if (typeof value === 'symbol') {
|
|
104
|
-
props[key] = symbolMarker(value);
|
|
105
|
-
} else if (value === null || typeof value !== 'object') {
|
|
106
|
-
props[key] = value;
|
|
107
|
-
} else if (value instanceof Date) {
|
|
108
|
-
props[key] = value;
|
|
109
|
-
} else if (Array.isArray(value)) {
|
|
110
|
-
props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
|
|
111
|
-
} else if (value instanceof Error) {
|
|
112
|
-
if (seen.has(value)) {
|
|
113
|
-
props[key] = '[Circular]';
|
|
114
|
-
} else {
|
|
115
|
-
props[key] = _extractErrorProps(value, seen, objectDepth, causeDepth, omit);
|
|
116
|
-
}
|
|
117
|
-
} else if (isPlainObject(value)) {
|
|
118
|
-
props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
|
|
121
|
+
for (const [key, value] of Object.entries(projected)){
|
|
122
|
+
if (value === undefined) continue;
|
|
123
|
+
if (key === 'cause') {
|
|
124
|
+
props.cause = cleanCause(value, seen, objectDepth, causeDepth, project);
|
|
119
125
|
} else {
|
|
120
|
-
|
|
121
|
-
// only the type name — but the key must not vanish: an absent key is indistinguishable
|
|
122
|
-
// from "no such field", and this output is now read by `get` as app-visible config data.
|
|
123
|
-
props[key] = classInstanceMarker(value);
|
|
126
|
+
props[key] = cleanPropValue(value, seen, objectDepth, causeDepth, project);
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
return props;
|
|
127
130
|
}
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
// project: (err) => object - the props to emit for THIS error node, called once
|
|
132
|
+
// per node. Entries are cleaned as own properties are: Errors recurse through
|
|
133
|
+
// the same `project`, the returned `cause` is walked in place of the original,
|
|
134
|
+
// and `undefined` entries are dropped. Returning neither `message` nor `name` is
|
|
135
|
+
// legal but degrades the wire format rather than just trimming it: serializer's
|
|
136
|
+
// propsToError revives a cause as an Error only when it has a `message`, and
|
|
137
|
+
// looks the Lowdefy error class up by `name`. Leave either out and a round-trip
|
|
138
|
+
// yields plain objects.
|
|
139
|
+
function extractErrorProps(err, { project } = {}) {
|
|
140
|
+
return _extractErrorProps(err, new Set(), 0, 0, project);
|
|
135
141
|
}
|
|
136
142
|
export default extractErrorProps;
|
|
@@ -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,30 +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';
|
|
41
|
+
import mapPlainValues from './mapPlainValues.js';
|
|
28
42
|
import mergeObjects from './mergeObjects.js';
|
|
43
|
+
import normalizeCaller from './normalizeCaller.js';
|
|
29
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';
|
|
51
|
+
import projectCaughtError from './projectCaughtError.js';
|
|
52
|
+
import rolesHash from './rolesHash.js';
|
|
30
53
|
import serializer from './serializer.js';
|
|
31
54
|
import set from './set.js';
|
|
32
55
|
import setKey from './setKey.js';
|
|
33
56
|
import splitPath from './splitPath.js';
|
|
34
57
|
import stableStringify from './stableStringify.js';
|
|
35
58
|
import swap from './swap.js';
|
|
59
|
+
import traceIdDate from './traceIdDate.js';
|
|
60
|
+
import targetFromElementsChain from './targetFromElementsChain.js';
|
|
36
61
|
import translate from './translate.js';
|
|
37
62
|
import type from './type.js';
|
|
38
63
|
import unset from './unset.js';
|
|
39
64
|
import unsetKey from './unsetKey.js';
|
|
40
65
|
import urlQuery from './urlQuery.js';
|
|
41
66
|
import wait from './wait.js';
|
|
42
|
-
export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getKey, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, getOperatorType, isReserved, joinPath, LRUCache, mergeObjects, omit, 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;
|