@jini-ai/admin 0.4.12 → 0.6.10
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/CHANGELOG.md +70 -2
- package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
- package/dist/agent-plugins/package-file-tree.js +54 -51
- package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
- package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
- package/dist/browser/navigation.js +44 -27
- package/dist/comments/adapters/memory.js +30 -12
- package/dist/comments/conformance/comments-api.conformance.js +55 -33
- package/dist/comments/controllers/comment-queue.controller.js +13 -4
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
- package/dist/comments/react/index.d.ts +2 -1
- package/dist/comments/react/index.js +1 -1
- package/dist/consent/react/ConsentFields.d.ts +35 -0
- package/dist/consent/react/ConsentFields.js +76 -0
- package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
- package/dist/consent/react/ConsentPreviewFrame.js +14 -0
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
- package/dist/consent/react/ConsentSettingsPanel.js +6 -17
- package/dist/consent/react/index.d.ts +6 -1
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
- package/dist/consent/react/settings-panel.hooks.js +26 -0
- package/dist/core/entities/rules.js +126 -81
- package/dist/core/manifest/rules.js +10 -10
- package/dist/core/module/create-admin.js +195 -117
- package/dist/core/ports/redirects.d.ts +8 -6
- package/dist/core/ports/redirects.js +8 -6
- package/dist/core/routing/rules.js +9 -4
- package/dist/core/transport/http.js +37 -20
- package/dist/forms/adapters/memory.js +22 -11
- package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
- package/dist/forms/conformance/forms-api.conformance.js +42 -46
- package/dist/forms/field-settings.js +150 -115
- package/dist/forms/html-rules.js +45 -28
- package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
- package/dist/forms/react/components/FieldSettingsRows.js +29 -0
- package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
- package/dist/forms/react/components/FieldSettingsSection.js +5 -6
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
- package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
- package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
- package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
- package/dist/forms/react/components/FormSubmissions.js +4 -20
- package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
- package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
- package/dist/media/adapters/memory.js +31 -26
- package/dist/media/controllers/edit-media.controller.js +2 -1
- package/dist/media/controllers/library.controller.js +48 -35
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.js +29 -15
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/react/components/MediaIcons.d.ts +0 -3
- package/dist/media/react/components/MediaIcons.js +0 -9
- package/dist/media/react/components/MediaLightbox.d.ts +2 -1
- package/dist/media/react/components/MediaLightbox.js +11 -5
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
- package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
- package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
- package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
- package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
- package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
- package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
- package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
- package/dist/media/react/index.js +4 -0
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/LibraryTab.js +2 -2
- package/dist/media/rules.d.ts +7 -0
- package/dist/media/rules.js +25 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
- package/dist/react/components/RowMenu/RowMenu.js +16 -10
- package/dist/react/components/Sidebar.js +32 -26
- package/dist/react/entities/EntityEdit.js +17 -14
- package/dist/react/entities/EntityField.d.ts +5 -2
- package/dist/react/entities/EntityField.js +38 -23
- package/dist/react/entities/EntityList.js +6 -5
- package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
- package/dist/react/entities/RelationTruncationNotice.js +9 -0
- package/dist/react/entities/use-entity-edit.d.ts +4 -2
- package/dist/react/entities/use-entity-edit.js +52 -33
- package/dist/react/shell/AdminShell.js +40 -23
- package/dist/react/shell/model.d.ts +4 -1
- package/dist/react/shell/model.js +3 -2
- package/dist/react/shell/use-admin-shell-session.js +25 -26
- package/dist/redirects/adapters/memory.js +9 -2
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
- package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
- package/dist/security/adapters/http.js +25 -10
- package/dist/security/adapters/memory.js +19 -5
- package/dist/security/controllers/access-tokens.controller.js +27 -26
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +17 -9
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
- package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.js +12 -3
- package/dist/security/rules.js +45 -25
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/react/components/MediaRefField.d.ts +1 -1
- package/dist/seo/react/components/MediaRefField.js +19 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
- package/dist/seo/react/pages/SeoPage.d.ts +4 -4
- package/dist/seo/react/pages/SeoPage.js +26 -6
- package/dist/seo/rules.js +39 -10
- package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
- package/dist/source-control/controllers/source-control.controller.js +20 -8
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
- package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
- package/dist/widgets/controllers/instance-editor.controller.js +33 -24
- package/dist/widgets/controllers/library.controller.js +18 -12
- package/dist/widgets/controllers/region-editor.controller.js +6 -5
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
- package/dist/widgets/rules.d.ts +14 -2
- package/dist/widgets/rules.js +9 -6
- package/package.json +23 -14
- package/src/themes/default/admin.css +51 -0
- package/src/themes/default/chat.css +21 -0
- package/src/themes/default/manifest.json +29 -0
- package/dist/media/styles.d.ts +0 -4
- package/dist/media/styles.js +0 -5
|
@@ -6,79 +6,116 @@ export class AdminConfigError extends Error {
|
|
|
6
6
|
this.issues = Object.freeze([...issues]);
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
9
|
+
function isValidToken(tok) {
|
|
10
|
+
return (Boolean(tok.id) &&
|
|
11
|
+
tok.cardinality === 'one' &&
|
|
12
|
+
Number.isInteger(tok.version) &&
|
|
13
|
+
tok.version >= 1);
|
|
14
|
+
}
|
|
15
|
+
function checkToken({ registry, name, tok }, _optional = {}) {
|
|
16
|
+
const { issues } = registry;
|
|
17
|
+
if (!isValidToken(tok))
|
|
18
|
+
issues.push(`Invalid token: ${name}`);
|
|
19
|
+
const old = registry.tokens.get(name);
|
|
20
|
+
if (old && (old.id !== tok.id || old.version !== tok.version))
|
|
21
|
+
issues.push(`Incompatible token: ${name}`);
|
|
22
|
+
const alias = registry.identities.get(tok.id);
|
|
23
|
+
if (alias && alias !== name)
|
|
24
|
+
issues.push(`Token ${tok.id} has conflicting names: ${alias}, ${name}`);
|
|
25
|
+
}
|
|
26
|
+
function collectTokens({ registry, map }, { required = false } = {}) {
|
|
27
|
+
for (const [name, tok] of Object.entries(map ?? {})) {
|
|
28
|
+
checkToken({ registry, name, tok });
|
|
29
|
+
registry.identities.set(tok.id, name);
|
|
30
|
+
registry.tokens.set(name, tok);
|
|
31
|
+
if (required)
|
|
32
|
+
registry.requiredNames.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function checkProvider({ registry, module, name, overrides, }, _optional = {}) {
|
|
36
|
+
const { issues, providers } = registry;
|
|
37
|
+
if (providers.has(name))
|
|
38
|
+
issues.push(`Duplicate provider: ${name}`);
|
|
39
|
+
providers.set(name, module);
|
|
40
|
+
const hosted = registry.host[name] != null;
|
|
41
|
+
if (hosted && !overrides.includes(name))
|
|
42
|
+
issues.push(`Provider collision: ${name}; declare an override`);
|
|
43
|
+
if (!module.factories?.[name] && !hosted)
|
|
44
|
+
issues.push(`Missing provider factory: ${name}`);
|
|
45
|
+
}
|
|
46
|
+
function registerProviders({ registry, module, overrides, }, _optional = {}) {
|
|
47
|
+
const provides = module.provides ?? {};
|
|
48
|
+
for (const name of Object.keys(provides))
|
|
49
|
+
checkProvider({ registry, module, name, overrides });
|
|
50
|
+
for (const name of Object.keys(module.factories ?? {}))
|
|
51
|
+
if (!(name in provides))
|
|
52
|
+
registry.issues.push(`Undeclared provider factory: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
function registerPages({ registry, module, omitted, }, _optional = {}) {
|
|
55
|
+
const { issues, paths, tabIds } = registry;
|
|
56
|
+
for (const [pageId, page] of Object.entries(module.pages)) {
|
|
57
|
+
if (paths.has(page.path))
|
|
58
|
+
issues.push(`Duplicate route: ${page.path}`);
|
|
59
|
+
paths.add(page.path);
|
|
60
|
+
for (const [tabId, tab] of Object.entries(page.tabs)) {
|
|
61
|
+
const full = `${module.id}.${pageId}.${tabId}`;
|
|
62
|
+
tabIds.add(full);
|
|
63
|
+
collectTokens({ registry, map: tab.requires }, { required: !omitted.has(full) });
|
|
64
|
+
collectTokens({ registry, map: tab.optional });
|
|
35
65
|
}
|
|
36
66
|
}
|
|
67
|
+
}
|
|
68
|
+
function registerModules({ modules, host, omitted, overrides, }, _optional = {}) {
|
|
69
|
+
const registry = {
|
|
70
|
+
host,
|
|
71
|
+
issues: [],
|
|
72
|
+
tokens: new Map(),
|
|
73
|
+
identities: new Map(),
|
|
74
|
+
providers: new Map(),
|
|
75
|
+
requiredNames: new Set(),
|
|
76
|
+
ids: new Set(),
|
|
77
|
+
paths: new Set(),
|
|
78
|
+
tabIds: new Set(),
|
|
79
|
+
};
|
|
37
80
|
for (const module of modules) {
|
|
38
|
-
if (ids.has(module.id))
|
|
39
|
-
issues.push(`Duplicate module: ${module.id}`);
|
|
40
|
-
ids.add(module.id);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
issues.push(`Duplicate provider: ${name}`);
|
|
47
|
-
providers.set(name, module);
|
|
48
|
-
if (host[name] != null &&
|
|
49
|
-
!options.overrides?.includes(name))
|
|
50
|
-
issues.push(`Provider collision: ${name}; declare an override`);
|
|
51
|
-
if (!module.factories?.[name] && host[name] == null)
|
|
52
|
-
issues.push(`Missing provider factory: ${name}`);
|
|
53
|
-
}
|
|
54
|
-
for (const name of Object.keys(module.factories ?? {}))
|
|
55
|
-
if (!(name in (module.provides ?? {})))
|
|
56
|
-
issues.push(`Undeclared provider factory: ${name}`);
|
|
57
|
-
for (const [pageId, page] of Object.entries(module.pages)) {
|
|
58
|
-
if (paths.has(page.path))
|
|
59
|
-
issues.push(`Duplicate route: ${page.path}`);
|
|
60
|
-
paths.add(page.path);
|
|
61
|
-
for (const [tabId, tab] of Object.entries(page.tabs)) {
|
|
62
|
-
const full = `${module.id}.${pageId}.${tabId}`;
|
|
63
|
-
tabIds.add(full);
|
|
64
|
-
collect(tab.requires, !omitted.has(full));
|
|
65
|
-
collect(tab.optional);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
81
|
+
if (registry.ids.has(module.id))
|
|
82
|
+
registry.issues.push(`Duplicate module: ${module.id}`);
|
|
83
|
+
registry.ids.add(module.id);
|
|
84
|
+
collectTokens({ registry, map: module.requires }, { required: true });
|
|
85
|
+
collectTokens({ registry, map: module.optional });
|
|
86
|
+
collectTokens({ registry, map: module.provides });
|
|
87
|
+
registerProviders({ registry, module, overrides });
|
|
88
|
+
registerPages({ registry, module, omitted });
|
|
68
89
|
}
|
|
90
|
+
return registry;
|
|
91
|
+
}
|
|
92
|
+
function checkOptions({ registry, omitted, overrides, }, _optional = {}) {
|
|
93
|
+
const { issues } = registry;
|
|
69
94
|
for (const id of omitted)
|
|
70
|
-
if (!tabIds.has(id))
|
|
95
|
+
if (!registry.tabIds.has(id))
|
|
71
96
|
issues.push(`Unknown tab: ${id}`);
|
|
72
|
-
for (const name of
|
|
73
|
-
if (!providers.has(
|
|
74
|
-
issues.push(`Unknown provider override: ${
|
|
97
|
+
for (const name of overrides)
|
|
98
|
+
if (!registry.providers.has(name))
|
|
99
|
+
issues.push(`Unknown provider override: ${name}`);
|
|
100
|
+
}
|
|
101
|
+
function checkPorts({ registry }, _optional = {}) {
|
|
102
|
+
const { host, issues, providers } = registry;
|
|
75
103
|
for (const name of Object.keys(host))
|
|
76
|
-
if (!tokens.has(name))
|
|
104
|
+
if (!registry.tokens.has(name))
|
|
77
105
|
issues.push(`Unknown port: ${name}`);
|
|
78
|
-
for (const name of requiredNames)
|
|
106
|
+
for (const name of registry.requiredNames)
|
|
79
107
|
if (host[name] == null && !providers.has(name))
|
|
80
108
|
issues.push(`Missing port: ${name}`);
|
|
81
|
-
|
|
109
|
+
}
|
|
110
|
+
function dependencies(module) {
|
|
111
|
+
const tabPorts = Object.values(module.pages)
|
|
112
|
+
.flatMap((page) => Object.values(page.tabs))
|
|
113
|
+
.reduce((result, tab) => ({ ...result, ...tab.requires, ...tab.optional }), {});
|
|
114
|
+
return { ...module.requires, ...module.optional, ...tabPorts };
|
|
115
|
+
}
|
|
116
|
+
/** Depth-first topological order of the providers the host does not already supply. */
|
|
117
|
+
function orderProviders({ registry }, _optional = {}) {
|
|
118
|
+
const { host, issues, providers } = registry;
|
|
82
119
|
const sorted = [], visiting = new Set(), visited = new Set();
|
|
83
120
|
function visit(name, chain) {
|
|
84
121
|
if (visited.has(name) || host[name] != null)
|
|
@@ -88,8 +125,7 @@ export function createAdmin({ modules, ports, }, options = {}) {
|
|
|
88
125
|
return;
|
|
89
126
|
}
|
|
90
127
|
visiting.add(name);
|
|
91
|
-
const
|
|
92
|
-
for (const dep of Object.keys(dependencies(module)))
|
|
128
|
+
for (const dep of Object.keys(dependencies(providers.get(name))))
|
|
93
129
|
if (providers.has(dep))
|
|
94
130
|
visit(dep, [...chain, name]);
|
|
95
131
|
visiting.delete(name);
|
|
@@ -98,68 +134,84 @@ export function createAdmin({ modules, ports, }, options = {}) {
|
|
|
98
134
|
}
|
|
99
135
|
for (const name of providers.keys())
|
|
100
136
|
visit(name, []);
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
return sorted;
|
|
138
|
+
}
|
|
139
|
+
function narrow({ module, values }) {
|
|
140
|
+
return Object.freeze(Object.fromEntries(Object.keys(dependencies(module)).map((name) => [name, values[name]])));
|
|
141
|
+
}
|
|
142
|
+
function isDisposable(value) {
|
|
143
|
+
return (typeof value === 'object' &&
|
|
144
|
+
value !== null &&
|
|
145
|
+
'dispose' in value &&
|
|
146
|
+
typeof value.dispose === 'function');
|
|
147
|
+
}
|
|
148
|
+
function disposeAll(disposables) {
|
|
149
|
+
for (const value of [...disposables].reverse())
|
|
150
|
+
value.dispose();
|
|
151
|
+
}
|
|
152
|
+
/** Runs factories in dependency order; a failure disposes what was built and reports one issue. */
|
|
153
|
+
function instantiate({ providers, sorted, values, grants, }, _optional = {}) {
|
|
103
154
|
const disposables = [];
|
|
104
|
-
function dependencies(module) {
|
|
105
|
-
const tabPorts = Object.values(module.pages)
|
|
106
|
-
.flatMap((page) => Object.values(page.tabs))
|
|
107
|
-
.reduce((result, tab) => ({ ...result, ...tab.requires, ...tab.optional }), {});
|
|
108
|
-
return { ...module.requires, ...module.optional, ...tabPorts };
|
|
109
|
-
}
|
|
110
|
-
function narrow(module) {
|
|
111
|
-
return Object.freeze(Object.fromEntries(Object.keys(dependencies(module)).map((name) => [name, values[name]])));
|
|
112
|
-
}
|
|
113
155
|
try {
|
|
114
156
|
for (const name of sorted) {
|
|
115
|
-
const
|
|
157
|
+
const module = providers.get(name);
|
|
158
|
+
const value = module.factories[name]({ ports: narrow({ module, values }), permissions: [...grants] }, {});
|
|
116
159
|
if (value == null)
|
|
117
160
|
throw new Error(`Provider returned no value: ${name}`);
|
|
118
161
|
values[name] = value;
|
|
119
|
-
if (
|
|
162
|
+
if (isDisposable(value))
|
|
120
163
|
disposables.push(value);
|
|
121
164
|
}
|
|
122
165
|
}
|
|
123
166
|
catch (error) {
|
|
124
|
-
|
|
125
|
-
value.dispose();
|
|
167
|
+
disposeAll(disposables);
|
|
126
168
|
throw new AdminConfigError({
|
|
127
169
|
issues: [error instanceof Error ? error.message : 'Provider initialization failed'],
|
|
128
170
|
});
|
|
129
171
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
return disposables;
|
|
173
|
+
}
|
|
174
|
+
const allowed = ({ grants }, permissions = []) => permissions.every((p) => grants.has(p));
|
|
175
|
+
function tabReason({ context, full, page, tab, }, _optional = {}) {
|
|
176
|
+
if (context.omitted.has(full))
|
|
177
|
+
return 'omitted';
|
|
178
|
+
if (!allowed(context, page.permissions) || !allowed(context, tab.permissions))
|
|
179
|
+
return 'permission denied';
|
|
180
|
+
const absent = Object.keys(tab.optional ?? {}).find((name) => context.values[name] == null);
|
|
181
|
+
return absent ? `optional port unavailable: ${absent}` : null;
|
|
182
|
+
}
|
|
183
|
+
function describePage({ context, module, id, page, }, _optional = {}) {
|
|
184
|
+
const visible = allowed(context, page.permissions);
|
|
185
|
+
return {
|
|
186
|
+
id: `${module.id}.${id}`,
|
|
187
|
+
path: page.path,
|
|
188
|
+
label: page.label,
|
|
189
|
+
visible,
|
|
190
|
+
agentReachable: page.agentReachable === true && visible,
|
|
191
|
+
permissions: page.permissions ?? [],
|
|
192
|
+
grants: Object.freeze([...context.grants]),
|
|
193
|
+
...(page.nav ? { nav: page.nav } : {}),
|
|
194
|
+
tabs: Object.entries(page.tabs).map(([tabId, tab]) => {
|
|
195
|
+
const full = `${module.id}.${id}.${tabId}`;
|
|
196
|
+
const reason = tabReason({ context, full, page, tab });
|
|
197
|
+
return {
|
|
198
|
+
id: full,
|
|
199
|
+
label: tab.label,
|
|
200
|
+
visible: reason === null,
|
|
201
|
+
reason,
|
|
202
|
+
params: tab.params ?? {},
|
|
203
|
+
agentReachable: reason === null && page.agentReachable === true && tab.agentReachable === true,
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function describeAdmin({ modules, registry, context, }, _optional = {}) {
|
|
209
|
+
const { host } = registry;
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
modules: Object.freeze([...registry.ids]),
|
|
212
|
+
pages: Object.freeze(modules.flatMap((module) => Object.entries(module.pages).map(([id, page]) => describePage({ context, module, id, page })))),
|
|
213
|
+
ports: Object.freeze([...registry.tokens]
|
|
214
|
+
.filter(([name]) => context.values[name] != null)
|
|
163
215
|
.map(([name, tok]) => ({
|
|
164
216
|
name,
|
|
165
217
|
token: tok.id,
|
|
@@ -167,19 +219,45 @@ export function createAdmin({ modules, ports, }, options = {}) {
|
|
|
167
219
|
source: host[name] != null ? 'host' : 'module',
|
|
168
220
|
}))),
|
|
169
221
|
});
|
|
222
|
+
}
|
|
223
|
+
export function createAdmin({ modules, ports, }, options = {}) {
|
|
224
|
+
const host = ports;
|
|
225
|
+
const values = { ...host };
|
|
226
|
+
const omitted = new Set(options.omit ?? []);
|
|
227
|
+
const overrides = (options.overrides ?? []).map(String);
|
|
228
|
+
const grants = new Set(options.permissions ?? []);
|
|
229
|
+
const registry = registerModules({ modules, host, omitted, overrides });
|
|
230
|
+
checkOptions({ registry, omitted, overrides });
|
|
231
|
+
checkPorts({ registry });
|
|
232
|
+
// Validate the graph before executing factories, so a boot error has no partial side effects.
|
|
233
|
+
const sorted = orderProviders({ registry });
|
|
234
|
+
if (registry.issues.length)
|
|
235
|
+
throw new AdminConfigError({ issues: [...new Set(registry.issues)] });
|
|
236
|
+
const disposables = instantiate({
|
|
237
|
+
providers: registry.providers,
|
|
238
|
+
sorted,
|
|
239
|
+
values,
|
|
240
|
+
grants: [...grants],
|
|
241
|
+
});
|
|
242
|
+
const description = describeAdmin({
|
|
243
|
+
modules,
|
|
244
|
+
registry,
|
|
245
|
+
context: { values, omitted, grants },
|
|
246
|
+
});
|
|
170
247
|
let disposed = false;
|
|
171
248
|
return {
|
|
172
249
|
describe: () => description,
|
|
173
250
|
scope: ({ module }) => {
|
|
174
251
|
if (disposed || !modules.includes(module))
|
|
175
|
-
throw new AdminConfigError({
|
|
176
|
-
|
|
252
|
+
throw new AdminConfigError({
|
|
253
|
+
issues: [`Module scope unavailable: ${module.id}`],
|
|
254
|
+
});
|
|
255
|
+
return narrow({ module, values });
|
|
177
256
|
},
|
|
178
257
|
dispose: () => {
|
|
179
258
|
if (!disposed) {
|
|
180
259
|
disposed = true;
|
|
181
|
-
|
|
182
|
-
value.dispose();
|
|
260
|
+
disposeAll(disposables);
|
|
183
261
|
}
|
|
184
262
|
},
|
|
185
263
|
};
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
* has no `slug` field at all) — kept here because the union is OPEN and a different host may
|
|
19
19
|
* legitimately implement `regex` even though the reference implementation refuses it.
|
|
20
20
|
*
|
|
21
|
-
* ## `tombstoneRedirect`
|
|
21
|
+
* ## `tombstoneRedirect` moves the rule to the Trash
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* The rule's `status` flips to `"disabled"` and the row is retained (audit trail, still readable
|
|
24
|
+
* via `getRedirect`), never hard-purged here — but it LEAVES `listRedirects` (Bug B, 2026-10-22):
|
|
25
|
+
* a removed rule is shown only in the host's Trash, unlike a rule merely toggled off, which stays
|
|
26
|
+
* listed as `"disabled"`. Removing a toggled-off rule moves it to the Trash too. A pure
|
|
27
|
+
* `updateRedirect({ status: "active" })` (or the host's Trash restore) brings it back to the list.
|
|
28
|
+
* The method resolves to the updated record (200-style), not `void`.
|
|
27
29
|
*
|
|
28
30
|
* ## `source: "auto_slug_change"` is read-only provenance
|
|
29
31
|
*
|
|
@@ -131,7 +133,7 @@ export interface AdminRedirectsPort {
|
|
|
131
133
|
updateRedirect(requiredArgs: {
|
|
132
134
|
id: string;
|
|
133
135
|
}, optionalArgs?: AdminRedirectUpdatePatch): Promise<AdminRedirect>;
|
|
134
|
-
/**
|
|
136
|
+
/** Moves the rule to the Trash (off `listRedirects`) — returns the tombstoned record, not `void`. See file header. */
|
|
135
137
|
tombstoneRedirect(requiredArgs: {
|
|
136
138
|
id: string;
|
|
137
139
|
}): Promise<AdminRedirect>;
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
* has no `slug` field at all) — kept here because the union is OPEN and a different host may
|
|
19
19
|
* legitimately implement `regex` even though the reference implementation refuses it.
|
|
20
20
|
*
|
|
21
|
-
* ## `tombstoneRedirect`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* ## `tombstoneRedirect` moves the rule to the Trash
|
|
22
|
+
*
|
|
23
|
+
* The rule's `status` flips to `"disabled"` and the row is retained (audit trail, still readable
|
|
24
|
+
* via `getRedirect`), never hard-purged here — but it LEAVES `listRedirects` (Bug B, 2026-10-22):
|
|
25
|
+
* a removed rule is shown only in the host's Trash, unlike a rule merely toggled off, which stays
|
|
26
|
+
* listed as `"disabled"`. Removing a toggled-off rule moves it to the Trash too. A pure
|
|
27
|
+
* `updateRedirect({ status: "active" })` (or the host's Trash restore) brings it back to the list.
|
|
28
|
+
* The method resolves to the updated record (200-style), not `void`.
|
|
27
29
|
*
|
|
28
30
|
* ## `source: "auto_slug_change"` is read-only provenance
|
|
29
31
|
*
|
|
@@ -46,8 +46,10 @@ export function adminHref({ routePath }, { base = DEFAULT_ADMIN_BASE } = {}) {
|
|
|
46
46
|
export function currentRoutePath({ pathname }, { base = DEFAULT_ADMIN_BASE } = {}) {
|
|
47
47
|
if (pathname === base)
|
|
48
48
|
return '/';
|
|
49
|
+
// The slice keeps the `/` the `startsWith` check just matched, so it is never empty and needs no
|
|
50
|
+
// `'/'` fallback.
|
|
49
51
|
if (pathname.startsWith(`${base}/`))
|
|
50
|
-
return pathname.slice(base.length)
|
|
52
|
+
return pathname.slice(base.length);
|
|
51
53
|
// Reached only if the app is mounted somewhere unexpected; treat the whole path as the route
|
|
52
54
|
// rather than silently returning the dashboard, so the mismatch is visible in the URL.
|
|
53
55
|
return pathname || '/';
|
|
@@ -63,7 +65,8 @@ export function currentRoutePath({ pathname }, { base = DEFAULT_ADMIN_BASE } = {
|
|
|
63
65
|
*/
|
|
64
66
|
export function matchRoute({ routePath, panels }) {
|
|
65
67
|
const [rawPath, rawQuery] = routePath.split('?');
|
|
66
|
-
|
|
68
|
+
// `split` with a string separator always returns at least one element, so the path part exists.
|
|
69
|
+
const segments = rawPath.split('/').filter(Boolean);
|
|
67
70
|
const query = new URLSearchParams(rawQuery ?? '');
|
|
68
71
|
const miss = (panelId) => ({ panelId, view: null, params: {}, query });
|
|
69
72
|
// Root is the dashboard by convention, and `dashboard` need not be registered for `/` to
|
|
@@ -99,8 +102,10 @@ function matchPattern(pattern, segments) {
|
|
|
99
102
|
return null;
|
|
100
103
|
const params = {};
|
|
101
104
|
for (let i = 0; i < patternSegments.length; i += 1) {
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
// Both indexes exist: the loop is bounded by `patternSegments.length`, which the length check
|
|
106
|
+
// above made equal to `segments.length`.
|
|
107
|
+
const patternSegment = patternSegments[i];
|
|
108
|
+
const segment = segments[i];
|
|
104
109
|
if (patternSegment.startsWith(':')) {
|
|
105
110
|
params[patternSegment.slice(1)] = segment;
|
|
106
111
|
continue;
|
|
@@ -18,16 +18,21 @@ function unreachableApiMessage({ status } = {}) {
|
|
|
18
18
|
return `cannot reach the API${detail} — is the server running?`;
|
|
19
19
|
}
|
|
20
20
|
/** Timeout DOMExceptions can come from another realm; instanceof Error alone is unreliable. */
|
|
21
|
+
function errorName(cause) {
|
|
22
|
+
return cause && typeof cause === 'object' && 'name' in cause ? cause.name : undefined;
|
|
23
|
+
}
|
|
24
|
+
function requestTimeoutError({ cause, timeoutMs }, _optional = {}) {
|
|
25
|
+
const message = `the API did not respond within ${timeoutMs / 1000}s — the browser may be out of free connections for this origin (try closing other admin tabs) or the server is unresponsive`;
|
|
26
|
+
return new AdminApiError({ message, status: 0 }, {
|
|
27
|
+
code: 'REQUEST_TIMEOUT', body: { cause: cause instanceof Error ? cause.message : String(cause) },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
21
30
|
function throwTranslatedFetchFailure({ cause, timeoutMs }, { isPageUnloading } = {}) {
|
|
22
|
-
const name = cause
|
|
31
|
+
const name = errorName(cause);
|
|
23
32
|
if (name === 'AbortError')
|
|
24
33
|
throw cause;
|
|
25
|
-
if (name === 'TimeoutError')
|
|
26
|
-
|
|
27
|
-
throw new AdminApiError({ message, status: 0 }, {
|
|
28
|
-
code: 'REQUEST_TIMEOUT', body: { cause: cause instanceof Error ? cause.message : String(cause) },
|
|
29
|
-
});
|
|
30
|
-
}
|
|
34
|
+
if (name === 'TimeoutError')
|
|
35
|
+
throw requestTimeoutError({ cause, timeoutMs });
|
|
31
36
|
// Only fetch's TypeError proves a network failure. Adapter bugs retain their original type.
|
|
32
37
|
if (!(cause instanceof TypeError))
|
|
33
38
|
throw cause;
|
|
@@ -37,6 +42,26 @@ function throwTranslatedFetchFailure({ cause, timeoutMs }, { isPageUnloading } =
|
|
|
37
42
|
code: 'API_UNREACHABLE', body: { cause: cause.message },
|
|
38
43
|
});
|
|
39
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Headers normalizes names and accepts every HeadersInit form; object spreads lose
|
|
47
|
+
* Headers instances and treat tuple arrays as numeric keys instead of header entries.
|
|
48
|
+
* Later sources win: the JSON default, then the transport's own, then the call's.
|
|
49
|
+
*/
|
|
50
|
+
function mergeHeaders(sources) {
|
|
51
|
+
const headers = new Headers({ 'Content-Type': 'application/json' });
|
|
52
|
+
for (const source of sources)
|
|
53
|
+
new Headers(source).forEach((value, name) => headers.set(name, value));
|
|
54
|
+
return headers;
|
|
55
|
+
}
|
|
56
|
+
/** The error a non-2xx response throws, given its parsed (or unparseable) body. */
|
|
57
|
+
function responseError({ res, body, wasUnparseable }, _optional = {}) {
|
|
58
|
+
// Valid JSON proves the application answered. Only unparseable 5xx responses indicate
|
|
59
|
+
// an unreachable upstream or a server crash outside the application's JSON envelope.
|
|
60
|
+
const noAppEnvelope = wasUnparseable && res.status >= 500;
|
|
61
|
+
const fallback = noAppEnvelope ? unreachableApiMessage({ status: res.status }) : `request failed (${res.status})`;
|
|
62
|
+
const serverCode = typeof body?.code === 'string' ? body.code : undefined;
|
|
63
|
+
return new AdminApiError({ message: String(body?.error ?? fallback), status: res.status }, { code: noAppEnvelope ? 'API_UNREACHABLE' : serverCode, body });
|
|
64
|
+
}
|
|
40
65
|
/**
|
|
41
66
|
* The standard `fetch`-backed transport.
|
|
42
67
|
*
|
|
@@ -44,14 +69,10 @@ function throwTranslatedFetchFailure({ cause, timeoutMs }, { isPageUnloading } =
|
|
|
44
69
|
* server's `error` string, HTTP status, canonical `code` when present, and the raw parsed body.
|
|
45
70
|
*/
|
|
46
71
|
export function createHttpTransport({ baseUrl, fetch: doFetch }, { headers: baseHeaders, credentials = 'same-origin', timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, isPageUnloading } = {}) {
|
|
72
|
+
const unloading = isPageUnloading ? { isPageUnloading } : {};
|
|
47
73
|
return {
|
|
48
74
|
async request({ path }, options = {}) {
|
|
49
75
|
const { onOk, ...init } = options;
|
|
50
|
-
// Headers normalizes names and accepts every HeadersInit form; object spreads lose
|
|
51
|
-
// Headers instances and treat tuple arrays as numeric keys instead of header entries.
|
|
52
|
-
const headers = new Headers({ 'Content-Type': 'application/json' });
|
|
53
|
-
new Headers(baseHeaders).forEach((value, name) => headers.set(name, value));
|
|
54
|
-
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
|
|
55
76
|
// An explicit signal owns cancellation and its deadline; never replace it with ours.
|
|
56
77
|
const signal = init.signal ?? AbortSignal.timeout(timeoutMs);
|
|
57
78
|
const res = await doFetch({ url: `${baseUrl}${path}` }, {
|
|
@@ -61,19 +82,15 @@ export function createHttpTransport({ baseUrl, fetch: doFetch }, { headers: base
|
|
|
61
82
|
// Assign last so a per-call header overrides both the JSON default and the transport's
|
|
62
83
|
// own. The reference implementation's original spread `...init` last, which meant a call passing `headers` lost
|
|
63
84
|
// the `Content-Type` default rather than merging with it.
|
|
64
|
-
headers,
|
|
65
|
-
}).catch((cause) => throwTranslatedFetchFailure({ cause, timeoutMs },
|
|
85
|
+
headers: mergeHeaders([baseHeaders, init.headers]),
|
|
86
|
+
}).catch((cause) => throwTranslatedFetchFailure({ cause, timeoutMs }, unloading));
|
|
66
87
|
// A 204 or an empty body is not a JSON parse failure worth surfacing — it is a successful
|
|
67
88
|
// mutation with nothing to return. Same `.catch(() => ({}))` as the original.
|
|
68
89
|
const parsed = await res.json().catch(() => UNPARSEABLE_BODY);
|
|
69
90
|
const wasUnparseable = parsed === UNPARSEABLE_BODY;
|
|
70
91
|
const body = (wasUnparseable ? {} : parsed);
|
|
71
|
-
if (!res.ok)
|
|
72
|
-
|
|
73
|
-
// an unreachable upstream or a server crash outside the application's JSON envelope.
|
|
74
|
-
const noAppEnvelope = wasUnparseable && res.status >= 500;
|
|
75
|
-
throw new AdminApiError({ message: String(body?.error ?? (noAppEnvelope ? unreachableApiMessage({ status: res.status }) : `request failed (${res.status})`)), status: res.status }, { code: noAppEnvelope ? 'API_UNREACHABLE' : typeof body?.code === 'string' ? body.code : undefined, body });
|
|
76
|
-
}
|
|
92
|
+
if (!res.ok)
|
|
93
|
+
throw responseError({ res, body, wasUnparseable });
|
|
77
94
|
onOk?.({ response: res });
|
|
78
95
|
return body;
|
|
79
96
|
},
|
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import { AdminApiError } from '../../core/transport/errors.js';
|
|
2
|
+
// The contract excludes slug; runtime callers cannot smuggle it through a cast either.
|
|
3
|
+
const PATCHABLE_KEYS = ['name', 'fields', 'notify', 'status', 'mode', 'html'];
|
|
4
|
+
/** The allowlisted patch keys the caller actually supplied (an explicit `undefined` is not a value). */
|
|
5
|
+
function suppliedPatch(patch) {
|
|
6
|
+
return Object.fromEntries(PATCHABLE_KEYS.filter(key => patch[key] !== undefined).map(key => [key, patch[key]]));
|
|
7
|
+
}
|
|
8
|
+
/** The core contract's add/edit-only invariant: a fields replacement must keep every current id. */
|
|
9
|
+
function assertNoFieldRemoved({ current, patch }) {
|
|
10
|
+
if (!patch.fields)
|
|
11
|
+
return;
|
|
12
|
+
const incomingIds = new Set(patch.fields.map(field => field.id));
|
|
13
|
+
if (current.fields.some(field => !incomingIds.has(field.id))) {
|
|
14
|
+
throw new AdminApiError({ message: 'existing form fields cannot be removed', status: 400 }, { code: 'FORMS_FIELD_VALIDATION_ERROR' });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
2
17
|
/** A scoped, deterministic in-memory API with the core contract's add/edit-only invariant. */
|
|
3
18
|
export function createMemoryFormsApi(_required = {}, { forms = [], submissions = [], now = () => new Date(0).toISOString(), nextId } = {}) {
|
|
4
19
|
const definitions = structuredClone([...forms]);
|
|
5
20
|
const answers = structuredClone([...submissions]);
|
|
6
21
|
let sequence = definitions.length;
|
|
7
|
-
const missing = (id
|
|
22
|
+
const missing = (id, { kind = 'form definition' } = {}) => {
|
|
23
|
+
throw new AdminApiError({ message: `${kind} '${id}' was not found`, status: 404 });
|
|
24
|
+
};
|
|
8
25
|
const find = (id) => definitions.find(form => form.slug === id) ?? definitions.find(form => form.id === id) ?? missing(id);
|
|
9
26
|
const definitionId = (id) => definitions.find(form => form.slug === id)?.id ?? id;
|
|
10
27
|
const findSubmission = (formId, id) => {
|
|
11
28
|
const resolvedId = definitionId(formId);
|
|
12
|
-
return answers.find(item => item.id === id && item.formDefinitionId === resolvedId && item.status !== 'trash') ?? missing(id);
|
|
29
|
+
return answers.find(item => item.id === id && item.formDefinitionId === resolvedId && item.status !== 'trash') ?? missing(id, { kind: 'form submission' });
|
|
13
30
|
};
|
|
14
31
|
const remove = (type, id) => {
|
|
15
32
|
const rows = type === 'form' ? definitions : answers;
|
|
16
33
|
const index = rows.findIndex(row => row.id === id);
|
|
17
34
|
if (index < 0)
|
|
18
|
-
missing(id);
|
|
35
|
+
missing(id, { kind: type === 'form' ? 'form definition' : 'form submission' });
|
|
19
36
|
// The fixture retains submission payloads just as the generic Trash owner does. Definitions
|
|
20
37
|
// keep their existing separate fixture projection; the core has no definition-delete method.
|
|
21
38
|
if (type === 'form_submission')
|
|
@@ -42,14 +59,8 @@ export function createMemoryFormsApi(_required = {}, { forms = [], submissions =
|
|
|
42
59
|
},
|
|
43
60
|
async updateFormDefinition({ id }, patch = {}) {
|
|
44
61
|
const current = definitions.find(form => form.id === id) ?? missing(id);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
throw new AdminApiError({ message: 'existing form fields cannot be removed', status: 400 }, { code: 'FORMS_FIELD_VALIDATION_ERROR' });
|
|
48
|
-
}
|
|
49
|
-
// The contract excludes slug; runtime callers cannot smuggle it through a cast either.
|
|
50
|
-
const { name, fields, notify, status, mode, html } = patch;
|
|
51
|
-
const applied = { ...(name !== undefined ? { name } : {}), ...(fields !== undefined ? { fields } : {}), ...(notify !== undefined ? { notify } : {}), ...(status !== undefined ? { status } : {}), ...(mode !== undefined ? { mode } : {}), ...(html !== undefined ? { html } : {}) };
|
|
52
|
-
const updated = { ...current, ...structuredClone(applied), updatedAt: now() };
|
|
62
|
+
assertNoFieldRemoved({ current, patch });
|
|
63
|
+
const updated = { ...current, ...structuredClone(suppliedPatch(patch)), updatedAt: now() };
|
|
53
64
|
definitions[definitions.indexOf(current)] = updated;
|
|
54
65
|
return structuredClone(updated);
|
|
55
66
|
},
|
|
@@ -3,15 +3,17 @@ export interface FormsConformanceResult {
|
|
|
3
3
|
readonly passed: number;
|
|
4
4
|
readonly failures: readonly string[];
|
|
5
5
|
}
|
|
6
|
+
type SubmissionSeed = {
|
|
7
|
+
formId: string;
|
|
8
|
+
submissionId: string;
|
|
9
|
+
};
|
|
6
10
|
/** Run on an isolated API: creates definitions and trashes seeded submissions.
|
|
7
11
|
* The host provides a disposable seed so conformance never touches an operator's live data. */
|
|
8
12
|
export declare function runFormsApiConformance({ api }: {
|
|
9
13
|
api: AdminFormsPort;
|
|
10
14
|
}, { slug, submission }?: {
|
|
11
15
|
slug?: string;
|
|
12
|
-
submission?:
|
|
13
|
-
formId: string;
|
|
14
|
-
submissionId: string;
|
|
15
|
-
};
|
|
16
|
+
submission?: SubmissionSeed;
|
|
16
17
|
}): Promise<FormsConformanceResult>;
|
|
18
|
+
export {};
|
|
17
19
|
//# sourceMappingURL=forms-api.conformance.d.ts.map
|