@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.
@@ -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;
@@ -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
- const NO_OMIT = [];
50
- function cleanValue(val, seen, objectDepth, causeDepth, omit) {
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, omit));
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, omit);
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, omit);
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 _extractErrorProps(err, seen, objectDepth, causeDepth, omit) {
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 applies the keys it is handed. Omitted fields
78
- // are skipped before they are built, so a dropped cause is never deep-copied
79
- // and never marked in `seen`.
80
- const omitted = omit?.(err) ?? NO_OMIT;
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
- if (!omitted.includes('message')) props.message = err.message;
83
- if (!omitted.includes('name')) props.name = err.name;
84
- if (!omitted.includes('stack')) props.stack = err.stack;
85
- if (err.cause !== undefined && !omitted.includes('cause')) {
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
- // A class instance (Socket, Agent, ClientRequest, ...). Its internals are never exposed —
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
- // omit: (err) => string[] - the field names to leave off THIS error node, called
129
- // once per node. Returning 'message' or 'name' is legal but degrades the wire
130
- // format rather than just trimming it: serializer's propsToError revives a cause
131
- // as an Error only when it has a `message`, and looks the Lowdefy error class up
132
- // by `name`. Omit either and a round-trip yields plain objects.
133
- function extractErrorProps(err, { omit } = {}) {
134
- return _extractErrorProps(err, new Set(), 0, 0, omit);
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;