@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.1
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/README.md +822 -135
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/config/portal-route-roots.json +48 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
- package/dist/base-skills/notis-apps/SKILL.md +93 -0
- package/dist/base-skills/notis-apps/references/architecture.md +114 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +97 -0
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +67 -0
- package/dist/base-skills/notis-apps/references/release.md +107 -0
- package/dist/base-skills/notis-apps/references/sdk.md +116 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
- package/dist/base-skills/notis-apps/references/views.md +142 -0
- package/dist/base-skills/notis-cli/SKILL.md +154 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +41 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
- package/dist/base-skills/notis-query/references/documents.md +63 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/Dialog.tsx +46 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
- package/dist/sdk/src/components/DocumentPage.tsx +141 -0
- package/dist/sdk/src/components/Markdown.tsx +60 -0
- package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
- package/dist/sdk/src/components/Skeleton.tsx +24 -0
- package/dist/sdk/src/config.ts +239 -0
- package/dist/sdk/src/documents.ts +258 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/sdk/src/hooks/useDocument.ts +43 -0
- package/dist/sdk/src/hooks/useDocuments.ts +84 -0
- package/dist/sdk/src/hooks/useHandover.ts +78 -0
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +42 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
- package/dist/sdk/src/hooks/useQuery.ts +74 -0
- package/dist/sdk/src/hooks/useTool.ts +65 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +200 -0
- package/dist/sdk/src/interactions/actions.ts +59 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/dist/sdk/src/interactions.ts +45 -0
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +49 -0
- package/dist/sdk/src/queryCache.ts +172 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +603 -0
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +290 -0
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/vite.ts +83 -0
- package/dist/skill-sync/index.js +2143 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +3487 -0
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1042 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +173 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +25 -8
- package/skills/notis-apps/cli.md +406 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +133 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +294 -25
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1476 -203
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +730 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +86 -82
- package/src/command-specs/index.js +31 -6
- package/src/command-specs/meta.js +150 -18
- package/src/command-specs/onboarding.js +290 -0
- package/src/command-specs/profile.js +358 -0
- package/src/command-specs/skills.js +304 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/spaces.js +572 -0
- package/src/command-specs/tools.js +455 -139
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +700 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-boundary-validator.js +407 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +2752 -237
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +293 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/errors.js +1 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +2622 -0
- package/src/runtime/output.js +37 -5
- package/src/runtime/ports.js +31 -0
- package/src/runtime/profiles.js +906 -55
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +121 -0
- package/src/runtime/skill-sync/index.ts +905 -0
- package/src/runtime/skill-sync/local-scanner.ts +1259 -0
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +59 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +54 -0
- package/src/runtime/skill-sync/types.ts +157 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/store-screenshot.js +146 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +331 -45
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/src/space-harness-host.ts +103 -0
- package/template/.harness/index.html.tmpl +435 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +6 -3
- package/template/app/page.tsx +49 -42
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +24 -6
- package/template/package-lock.json +3642 -0
- package/template/package.json +19 -16
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/src/command-specs/db.js +0 -163
- package/src/runtime/app-preview-server.js +0 -312
- package/template/packages/notis-sdk/src/config.ts +0 -48
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
- package/template/packages/notis-sdk/src/index.ts +0 -47
- package/template/packages/notis-sdk/src/provider.tsx +0 -44
- package/template/packages/notis-sdk/src/runtime.ts +0 -159
- package/template/packages/notis-sdk/src/styles.css +0 -123
- package/template/packages/notis-sdk/src/vite.ts +0 -54
- package/template/packages/notis-sdk/tsconfig.json +0 -15
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
package/src/runtime/transport.js
CHANGED
|
@@ -1,37 +1,96 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
2
4
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
|
+
import { delegatedContextReason } from './delegated-context.js';
|
|
6
|
+
import {
|
|
7
|
+
credentialIsExpired,
|
|
8
|
+
getJwtExpiration,
|
|
9
|
+
getJwtSubject,
|
|
10
|
+
getProfile,
|
|
11
|
+
isJwtExpired,
|
|
12
|
+
loadConfig,
|
|
13
|
+
resolveWorktreeRuntime,
|
|
14
|
+
} from './profiles.js';
|
|
15
|
+
import { createExpiredAuthError, createInvalidAuthHints } from './auth-recovery.js';
|
|
16
|
+
import { refreshOAuthCredential } from './oauth.js';
|
|
3
17
|
|
|
4
|
-
|
|
5
|
-
'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
function escapeMultipartHeaderValue(value) {
|
|
19
|
+
return String(value ?? '')
|
|
20
|
+
.replace(/"/g, '%22')
|
|
21
|
+
.replace(/\r/g, '%0D')
|
|
22
|
+
.replace(/\n/g, '%0A');
|
|
23
|
+
}
|
|
10
24
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
function multipartTextPart(boundary, name, value) {
|
|
26
|
+
return [
|
|
27
|
+
`--${boundary}`,
|
|
28
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
|
|
29
|
+
'',
|
|
30
|
+
value,
|
|
31
|
+
'',
|
|
32
|
+
].join('\r\n');
|
|
33
|
+
}
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
function multipartFileHeader(boundary, binding) {
|
|
36
|
+
const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
|
|
37
|
+
return [
|
|
38
|
+
`--${boundary}`,
|
|
39
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
|
|
40
|
+
`Content-Type: ${binding.contentType || 'application/octet-stream'}`,
|
|
41
|
+
'',
|
|
42
|
+
'',
|
|
43
|
+
].join('\r\n');
|
|
44
|
+
}
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
|
|
47
|
+
yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
|
|
48
|
+
yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
|
|
49
|
+
for (const binding of fileBindings) {
|
|
50
|
+
yield Buffer.from(multipartFileHeader(boundary, binding));
|
|
51
|
+
for await (const chunk of createReadStream(binding.localPath)) {
|
|
52
|
+
yield chunk;
|
|
53
|
+
}
|
|
54
|
+
yield Buffer.from('\r\n');
|
|
22
55
|
}
|
|
56
|
+
yield Buffer.from(`--${boundary}--\r\n`);
|
|
57
|
+
}
|
|
23
58
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
|
|
60
|
+
const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
|
|
61
|
+
const textParts = [
|
|
62
|
+
multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
|
|
63
|
+
multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
|
|
64
|
+
];
|
|
65
|
+
const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
|
|
66
|
+
const closingPart = `--${boundary}--\r\n`;
|
|
67
|
+
const contentLength = [
|
|
68
|
+
...textParts,
|
|
69
|
+
...fileHeaderParts,
|
|
70
|
+
...fileBindings.map(() => '\r\n'),
|
|
71
|
+
closingPart,
|
|
72
|
+
].reduce((total, part) => total + Buffer.byteLength(part), 0)
|
|
73
|
+
+ fileBindings.reduce((total, binding) => total + binding.size, 0);
|
|
27
74
|
|
|
28
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
79
|
+
'Content-Length': String(contentLength),
|
|
80
|
+
},
|
|
81
|
+
duplex: 'half',
|
|
82
|
+
};
|
|
29
83
|
}
|
|
30
84
|
|
|
31
|
-
function normalizeBackendError(status, payload) {
|
|
85
|
+
export function normalizeBackendError(status, payload, runtime) {
|
|
32
86
|
const backendError = payload?.error;
|
|
87
|
+
// Space errors answer {error: <stable code>, message: <readable text>}: show
|
|
88
|
+
// the text (it can name the Space involved); the code stays in details.
|
|
89
|
+
const codedMessage = typeof backendError === 'string' && /^[a-z][a-z0-9_]*$/.test(backendError)
|
|
90
|
+
&& typeof payload?.message === 'string' && payload.message ? payload.message : null;
|
|
33
91
|
const message =
|
|
34
92
|
backendError?.message ||
|
|
93
|
+
codedMessage ||
|
|
35
94
|
backendError ||
|
|
36
95
|
payload?.message ||
|
|
37
96
|
payload?.error ||
|
|
@@ -43,10 +102,7 @@ function normalizeBackendError(status, payload) {
|
|
|
43
102
|
message,
|
|
44
103
|
exitCode: EXIT_CODES.auth,
|
|
45
104
|
hints: [
|
|
46
|
-
|
|
47
|
-
command: 'notis auth login --jwt <token>',
|
|
48
|
-
reason: 'Refresh the stored JWT for this profile',
|
|
49
|
-
},
|
|
105
|
+
...createInvalidAuthHints(runtime),
|
|
50
106
|
],
|
|
51
107
|
details: payload || {},
|
|
52
108
|
});
|
|
@@ -77,6 +133,18 @@ function normalizeBackendError(status, payload) {
|
|
|
77
133
|
});
|
|
78
134
|
}
|
|
79
135
|
|
|
136
|
+
if (status === 402) {
|
|
137
|
+
// Distinct from auth on purpose: an agent that sees an auth exit code will
|
|
138
|
+
// loop trying to re-authenticate, when the actual fix is to add credit.
|
|
139
|
+
return new CliError({
|
|
140
|
+
code: 'payment_required',
|
|
141
|
+
message,
|
|
142
|
+
exitCode: EXIT_CODES.payment,
|
|
143
|
+
details: payload || {},
|
|
144
|
+
hints: payload?.hints || [],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
80
148
|
if (status >= 400 && status < 500) {
|
|
81
149
|
return new CliError({
|
|
82
150
|
code: 'usage_error',
|
|
@@ -97,45 +165,235 @@ function normalizeBackendError(status, payload) {
|
|
|
97
165
|
});
|
|
98
166
|
}
|
|
99
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Pick up a dev credential that `./dev.sh` re-minted mid-process.
|
|
170
|
+
*
|
|
171
|
+
* Restarting the local runtime rewrites the worktree lease in place, so a
|
|
172
|
+
* long-running command can recover from a single 401 instead of failing and
|
|
173
|
+
* making the caller rerun it. The refreshed token still has to belong to the
|
|
174
|
+
* worktree's approved test user.
|
|
175
|
+
*/
|
|
176
|
+
function reloadDevJwt(runtime, loadedProfile = null) {
|
|
177
|
+
if (runtime.credentialKind !== 'worktree') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
let nextJwt = null;
|
|
181
|
+
const expectedUserId = runtime.worktreeRuntime?.expected_user_id;
|
|
182
|
+
const runtimePath = runtime.worktreeRuntime?.runtime_path;
|
|
183
|
+
if (runtimePath) {
|
|
184
|
+
const refreshedRuntime = resolveWorktreeRuntime(dirname(runtimePath));
|
|
185
|
+
if (
|
|
186
|
+
refreshedRuntime
|
|
187
|
+
&& !refreshedRuntime.unavailable
|
|
188
|
+
&& refreshedRuntime.runtime_path === runtimePath
|
|
189
|
+
&& refreshedRuntime.profile === runtime.profileName
|
|
190
|
+
&& refreshedRuntime.api_base === runtime.apiBase
|
|
191
|
+
) {
|
|
192
|
+
if (
|
|
193
|
+
expectedUserId
|
|
194
|
+
&& refreshedRuntime.expected_user_id !== expectedUserId
|
|
195
|
+
) {
|
|
196
|
+
throw new CliError({
|
|
197
|
+
code: 'dev_runtime_identity_mismatch',
|
|
198
|
+
message: 'The restarted worktree runtime belongs to a different test user',
|
|
199
|
+
exitCode: EXIT_CODES.auth,
|
|
200
|
+
hints: [
|
|
201
|
+
{ message: 'Restart this CLI command under the worktree\'s current dev identity.' },
|
|
202
|
+
{ message: `Expected user: ${expectedUserId}` },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
runtime.worktreeRuntime = refreshedRuntime;
|
|
207
|
+
nextJwt = refreshedRuntime.dev_access_token;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let profile = loadedProfile;
|
|
211
|
+
if (!nextJwt && !profile) {
|
|
212
|
+
try {
|
|
213
|
+
profile = getProfile(loadConfig(), runtime.profileName);
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
nextJwt = nextJwt || (
|
|
219
|
+
typeof profile?.dev_access_token === 'string' && profile.dev_access_token
|
|
220
|
+
? profile.dev_access_token
|
|
221
|
+
: null
|
|
222
|
+
);
|
|
223
|
+
if (!nextJwt || nextJwt === runtime.jwt) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (expectedUserId && getJwtSubject(nextJwt) !== expectedUserId) {
|
|
227
|
+
throw new CliError({
|
|
228
|
+
code: 'dev_runtime_identity_mismatch',
|
|
229
|
+
message: 'The refreshed dev credential does not belong to this worktree test user',
|
|
230
|
+
exitCode: EXIT_CODES.auth,
|
|
231
|
+
hints: [
|
|
232
|
+
{ message: 'Restart ./dev.sh to restore the approved worktree identity.' },
|
|
233
|
+
{ message: `Expected user: ${expectedUserId}` },
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
runtime.jwt = nextJwt;
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A live worktree lease is authoritative over any stale dev metadata left in
|
|
243
|
+
* the shared profile store. Prefer its explicit expiry, then the active JWT's
|
|
244
|
+
* expiry, and deliberately fail closed when neither can be verified.
|
|
245
|
+
*/
|
|
246
|
+
export function getActiveWorktreeCredentialProfile(runtime, loadedProfile = {}) {
|
|
247
|
+
if (runtime.credentialKind !== 'worktree') {
|
|
248
|
+
return loadedProfile;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
...loadedProfile,
|
|
252
|
+
dev_access_expires_at:
|
|
253
|
+
runtime.worktreeRuntime?.dev_access_expires_at
|
|
254
|
+
?? getJwtExpiration(runtime.jwt)
|
|
255
|
+
?? loadedProfile.dev_access_expires_at,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
100
259
|
export async function httpRequest({
|
|
101
260
|
runtime,
|
|
102
261
|
method = 'POST',
|
|
103
262
|
path,
|
|
104
263
|
body,
|
|
264
|
+
multipart = false,
|
|
105
265
|
requireAuth = true,
|
|
266
|
+
spacesProtocol,
|
|
106
267
|
}) {
|
|
107
|
-
|
|
108
|
-
|
|
268
|
+
// Refresh before spending the credential rather than after a rejection: the
|
|
269
|
+
// rotating refresh token is shared with every other `notis` process reading
|
|
270
|
+
// the same profile, so a lapsed access token is renewed once, under the
|
|
271
|
+
// config write lock, instead of racing on a 401 retry.
|
|
272
|
+
let currentProfile;
|
|
273
|
+
if (runtime.credentialKind === 'oauth') {
|
|
274
|
+
if (requireAuth && credentialIsExpired(runtime, {
|
|
275
|
+
oauth_access_expires_at: runtime.oauthAccessExpiresAt,
|
|
276
|
+
})) {
|
|
277
|
+
await refreshOAuthCredential(runtime);
|
|
278
|
+
}
|
|
279
|
+
currentProfile = getProfile(loadConfig(), runtime.profileName);
|
|
280
|
+
} else {
|
|
281
|
+
currentProfile = getProfile(loadConfig(), runtime.profileName);
|
|
282
|
+
reloadDevJwt(runtime, currentProfile);
|
|
283
|
+
currentProfile = getActiveWorktreeCredentialProfile(runtime, currentProfile);
|
|
284
|
+
}
|
|
285
|
+
if (requireAuth && credentialIsExpired(runtime, currentProfile)) {
|
|
286
|
+
throw createExpiredAuthError(runtime);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let controller = new AbortController();
|
|
290
|
+
let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
109
291
|
const requestId = `req_${randomUUID().replace(/-/g, '')}`;
|
|
292
|
+
const resetTimeout = () => {
|
|
293
|
+
clearTimeout(timeout);
|
|
294
|
+
controller = new AbortController();
|
|
295
|
+
timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
296
|
+
};
|
|
110
297
|
|
|
111
298
|
const headers = {
|
|
112
|
-
'Content-Type': 'application/json',
|
|
113
299
|
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
114
300
|
'X-Notis-Request-Id': requestId,
|
|
115
301
|
};
|
|
302
|
+
if (spacesProtocol !== undefined) headers['X-Notis-Spaces-Protocol'] = String(spacesProtocol);
|
|
303
|
+
if (!multipart) {
|
|
304
|
+
headers['Content-Type'] = 'application/json';
|
|
305
|
+
}
|
|
116
306
|
|
|
117
307
|
if (requireAuth && runtime.jwt) {
|
|
118
308
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
119
309
|
}
|
|
120
310
|
|
|
311
|
+
const resolveBody = () => {
|
|
312
|
+
if (!multipart) {
|
|
313
|
+
return {
|
|
314
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
315
|
+
headers: {},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (typeof body === 'function') {
|
|
319
|
+
return body();
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
body,
|
|
323
|
+
headers: {},
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
121
327
|
try {
|
|
122
|
-
|
|
328
|
+
let requestBody = resolveBody();
|
|
329
|
+
let response = await fetch(`${runtime.apiBase}${path}`, {
|
|
123
330
|
method,
|
|
124
|
-
headers,
|
|
125
|
-
body:
|
|
331
|
+
headers: { ...headers, ...requestBody.headers },
|
|
332
|
+
body: requestBody.body,
|
|
126
333
|
signal: controller.signal,
|
|
334
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
127
335
|
});
|
|
128
|
-
clearTimeout(timeout);
|
|
129
336
|
|
|
130
337
|
let payload = null;
|
|
338
|
+
let payloadReadError = null;
|
|
131
339
|
try {
|
|
132
340
|
payload = await response.json();
|
|
133
|
-
} catch {
|
|
341
|
+
} catch (error) {
|
|
342
|
+
payloadReadError = error;
|
|
134
343
|
payload = null;
|
|
135
344
|
}
|
|
136
345
|
|
|
346
|
+
if (response.status === 401) {
|
|
347
|
+
const refreshed = runtime.credentialKind === 'oauth'
|
|
348
|
+
? await refreshOAuthCredential(runtime)
|
|
349
|
+
: reloadDevJwt(runtime) && !isJwtExpired(runtime.jwt);
|
|
350
|
+
if (refreshed) {
|
|
351
|
+
if (requireAuth && runtime.jwt) {
|
|
352
|
+
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
353
|
+
}
|
|
354
|
+
resetTimeout();
|
|
355
|
+
requestBody = resolveBody();
|
|
356
|
+
response = await fetch(`${runtime.apiBase}${path}`, {
|
|
357
|
+
method,
|
|
358
|
+
headers: { ...headers, ...requestBody.headers },
|
|
359
|
+
body: requestBody.body,
|
|
360
|
+
signal: controller.signal,
|
|
361
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
362
|
+
});
|
|
363
|
+
payloadReadError = null;
|
|
364
|
+
try {
|
|
365
|
+
payload = await response.json();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
payloadReadError = error;
|
|
368
|
+
payload = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
clearTimeout(timeout);
|
|
374
|
+
|
|
137
375
|
if (!response.ok) {
|
|
138
|
-
|
|
376
|
+
if (
|
|
377
|
+
response.status === 401
|
|
378
|
+
&& credentialIsExpired(
|
|
379
|
+
runtime,
|
|
380
|
+
getActiveWorktreeCredentialProfile(
|
|
381
|
+
runtime,
|
|
382
|
+
getProfile(loadConfig(), runtime.profileName),
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
) {
|
|
386
|
+
throw createExpiredAuthError(runtime);
|
|
387
|
+
}
|
|
388
|
+
throw normalizeBackendError(response.status, payload, runtime);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// A successful status is not a successful tool call until its response
|
|
392
|
+
// body has been read. Mutations may already have committed before a socket
|
|
393
|
+
// reset truncates the JSON; surface that as an ambiguous network failure
|
|
394
|
+
// so callers never persist an undefined result or attempt a second write.
|
|
395
|
+
if (response.status !== 204 && payloadReadError) {
|
|
396
|
+
throw payloadReadError;
|
|
139
397
|
}
|
|
140
398
|
|
|
141
399
|
return {
|
|
@@ -177,22 +435,50 @@ export async function callTool({
|
|
|
177
435
|
toolName,
|
|
178
436
|
arguments_: argumentsPayload = {},
|
|
179
437
|
idempotencyKey,
|
|
438
|
+
fileBindings = [],
|
|
180
439
|
}) {
|
|
181
440
|
const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
|
|
441
|
+
const payload = {
|
|
442
|
+
tool_name: toolName,
|
|
443
|
+
arguments: argumentsPayload,
|
|
444
|
+
idempotency_key: requestId,
|
|
445
|
+
cli_context: {
|
|
446
|
+
output_mode: runtime.outputMode,
|
|
447
|
+
profile: runtime.profileName,
|
|
448
|
+
cwd: process.cwd(),
|
|
449
|
+
agent_mode: runtime.agentMode,
|
|
450
|
+
cli_version: runtime.cliVersion,
|
|
451
|
+
// Reported so the server can refuse work-handing tools from a run Notis
|
|
452
|
+
// is itself driving. Advisory -- a determined agent can unset the markers
|
|
453
|
+
// -- so the server treats it as one layer, not the whole guard.
|
|
454
|
+
...(delegatedContextReason() ? { delegated_context: true } : {}),
|
|
455
|
+
...(runtime.debugEntitlementOverride
|
|
456
|
+
? { debug_entitlement_override: runtime.debugEntitlementOverride }
|
|
457
|
+
: {}),
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
if (Array.isArray(fileBindings) && fileBindings.length) {
|
|
462
|
+
const bindingMetadata = fileBindings.map((binding) => {
|
|
463
|
+
const {
|
|
464
|
+
contentType,
|
|
465
|
+
localPath,
|
|
466
|
+
...metadata
|
|
467
|
+
} = binding;
|
|
468
|
+
return metadata;
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return httpRequest({
|
|
472
|
+
runtime,
|
|
473
|
+
path: '/cli_tools',
|
|
474
|
+
body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
|
|
475
|
+
multipart: true,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
182
479
|
return httpRequest({
|
|
183
480
|
runtime,
|
|
184
481
|
path: '/cli_tools',
|
|
185
|
-
body:
|
|
186
|
-
tool_name: canonicalizeToolName(toolName),
|
|
187
|
-
arguments: argumentsPayload,
|
|
188
|
-
idempotency_key: requestId,
|
|
189
|
-
cli_context: {
|
|
190
|
-
output_mode: runtime.outputMode,
|
|
191
|
-
profile: runtime.profileName,
|
|
192
|
-
cwd: process.cwd(),
|
|
193
|
-
agent_mode: runtime.agentMode,
|
|
194
|
-
cli_version: runtime.cliVersion,
|
|
195
|
-
},
|
|
196
|
-
},
|
|
482
|
+
body: payload,
|
|
197
483
|
});
|
|
198
484
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { httpRequest } from './transport.js';
|
|
6
|
+
import { CliError, EXIT_CODES, usageError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
async function rendererLibrary() {
|
|
9
|
+
const packaged = new URL('../../dist/view-renderer/src/index.js', import.meta.url);
|
|
10
|
+
if (existsSync(fileURLToPath(packaged))) return import(packaged.href);
|
|
11
|
+
const development = new URL('../../../view-renderer/src/index.js', import.meta.url);
|
|
12
|
+
if (existsSync(fileURLToPath(development))) return import(development.href);
|
|
13
|
+
throw usageError('The shared view renderer is missing. Rebuild or reinstall this CLI package.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Root command registration supplies flags/output formatting; this adapter holds no browser JWT. */
|
|
17
|
+
export async function renderViewWithCliRuntime({ runtime, url, outputs = ['markdown', 'screenshot'], width = 1440,
|
|
18
|
+
directory, signal, loadLibrary = rendererLibrary, request = httpRequest }) {
|
|
19
|
+
if (typeof url !== 'string' || !url.trim() || ![390, 1440].includes(width)) throw usageError('Choose a view link and width 390 or 1440.');
|
|
20
|
+
const ticket = (await request({ runtime, method: 'POST', path: '/portal_view_render/prepare', spacesProtocol: 1,
|
|
21
|
+
body: { url, outputs, width }, signal })).payload;
|
|
22
|
+
if (typeof ticket?.credential !== 'string') throw usageError('The short-lived render ticket is missing.');
|
|
23
|
+
const library = await loadLibrary();
|
|
24
|
+
const broker = library.createRemoteRenderBroker({ apiBase: runtime.apiBase, credential: ticket.credential });
|
|
25
|
+
let result;
|
|
26
|
+
try {
|
|
27
|
+
result = await library.renderView({ broker, outputs, width, signal });
|
|
28
|
+
await broker.complete(result, { signal });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof library.ViewRenderError) throw new CliError({ code: error.code,
|
|
32
|
+
message: error.message, exitCode: EXIT_CODES.backend, retryable: error.status >= 500 || error.status === 429, cause: error });
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
if (!directory) return { result, artifacts: null };
|
|
36
|
+
const root = resolve(directory), created = [], artifacts = {};
|
|
37
|
+
await mkdir(root, { recursive: true });
|
|
38
|
+
try {
|
|
39
|
+
for (const [kind, name, bytes] of [
|
|
40
|
+
['markdown', 'view.md', result.markdown === undefined ? undefined : Buffer.from(result.markdown, 'utf8')],
|
|
41
|
+
['screenshot', 'view.png', result.screenshot ? Buffer.from(result.screenshot.data_base64, 'base64') : undefined],
|
|
42
|
+
]) {
|
|
43
|
+
if (bytes === undefined) continue;
|
|
44
|
+
const path = join(root, name);
|
|
45
|
+
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 }); created.push(path); artifacts[kind] = path;
|
|
46
|
+
}
|
|
47
|
+
} catch (error) { await Promise.allSettled(created.map(path => unlink(path))); throw error; }
|
|
48
|
+
return { result, artifacts };
|
|
49
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { resolveRuntimeProfile } from './runtime/profiles.js';
|
|
5
|
+
import { ensureFreshOAuthCredential } from './runtime/oauth.js';
|
|
6
|
+
import { runSkillSync, fetchSyncSettings } from '../dist/skill-sync/index.js';
|
|
7
|
+
import { withSkillSyncLock } from './runtime/sync-skills.js';
|
|
8
|
+
|
|
9
|
+
export async function runAutomaticSkillSync({ profile, apiBase, userId }, {
|
|
10
|
+
resolveRuntime = resolveRuntimeProfile, refresh = ensureFreshOAuthCredential,
|
|
11
|
+
settings = fetchSyncSettings, sync = runSkillSync, lock = withSkillSyncLock,
|
|
12
|
+
} = {}) {
|
|
13
|
+
const runtime = resolveRuntime({ profile }, { requireAuth: true });
|
|
14
|
+
if (runtime.credentialKind !== 'oauth' || runtime.apiBase !== apiBase
|
|
15
|
+
|| runtime.oauthUserId !== userId) throw new Error('Automatic skill sync account changed; run notis skills sync to rebind');
|
|
16
|
+
await refresh(runtime);
|
|
17
|
+
const saved = await settings(runtime.apiBase, runtime.jwt);
|
|
18
|
+
if (saved.user_id !== userId) throw new Error('Automatic skill sync identity mismatch');
|
|
19
|
+
// Do not gather, unlink foreign accounts or write anything when opted out.
|
|
20
|
+
if (!saved.sync_enabled) return { status: 'disabled' };
|
|
21
|
+
const result = await lock(() => sync(runtime.apiBase, runtime.jwt, {
|
|
22
|
+
fetchSyncSettings: async () => saved,
|
|
23
|
+
}, { honorSyncEnabled: true }));
|
|
24
|
+
return { status: (result.failedLinks?.length || result.failedPushes?.length) ? 'partial' : 'synced', ...result };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function main(args = process.argv.slice(2)) {
|
|
28
|
+
const [profile, apiBase, userId] = args;
|
|
29
|
+
const root = join(homedir(), '.notis', 'skills', 'service');
|
|
30
|
+
const record = (value) => {
|
|
31
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
32
|
+
const target = join(root, 'status.json');
|
|
33
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
34
|
+
writeFileSync(temporary, JSON.stringify({ ...value, profile, at: new Date().toISOString() }, null, 2), { mode: 0o600 });
|
|
35
|
+
renameSync(temporary, target);
|
|
36
|
+
};
|
|
37
|
+
const deadline = setTimeout(() => {
|
|
38
|
+
record({ status: 'error', code: 'sync_timeout' });
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}, 240_000);
|
|
41
|
+
try {
|
|
42
|
+
record(await runAutomaticSkillSync({ profile, apiBase, userId }));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Persist only a classified error, never a bearer, signed URL or response body.
|
|
45
|
+
record({ status: 'error', code: error.code || 'sync_failed' });
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(deadline);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as ReactDOM from 'react-dom';
|
|
3
|
+
import * as ReactDOMClient from 'react-dom/client';
|
|
4
|
+
import * as JSXRuntime from 'react/jsx-runtime';
|
|
5
|
+
import { importBundleSource, resolveBundleRouteExport } from '../../sdk/src/bundleModule';
|
|
6
|
+
import { SdkPresentation } from '../../sdk/src/presentation';
|
|
7
|
+
import { createPrefetchQueue, createQueryClient } from '../../sdk/src/queryCache';
|
|
8
|
+
import type { NotisRuntime } from '../../sdk/src/runtime';
|
|
9
|
+
import { SPACE_VIEWER_READ_OPERATIONS } from '../../sdk/src/space';
|
|
10
|
+
import type { SpaceActionOptions, SpaceDocumentBodyRequest, SpaceViewerReadOperation } from '../../sdk/src/space';
|
|
11
|
+
|
|
12
|
+
type Call = { op: string; actionId?: string; ok: boolean | null; error?: string };
|
|
13
|
+
const state = { mounted: false, renderStarted: false, errors: [] as Array<{ phase: string; message: string }>, runtimeCalls: [] as Call[] };
|
|
14
|
+
Object.assign(window, { __harness: state, React, ReactDOM, ReactDOMClient, __ReactJSXRuntime: JSXRuntime });
|
|
15
|
+
const diagnostic = (phase: string, reason: unknown) => state.errors.push({ phase,
|
|
16
|
+
message: reason instanceof Error ? reason.message : String(reason || 'Verification failed.') });
|
|
17
|
+
window.addEventListener('error', event => diagnostic('window_error', event.error || event.message));
|
|
18
|
+
window.addEventListener('unhandledrejection', event => diagnostic('unhandled_rejection', event.reason));
|
|
19
|
+
window.addEventListener('securitypolicyviolation', event => diagnostic('security_policy', `Blocked ${event.violatedDirective}`));
|
|
20
|
+
|
|
21
|
+
async function start() {
|
|
22
|
+
const response = await fetch('/snapshot', { credentials: 'omit' });
|
|
23
|
+
if (!response.ok) throw new Error('The frozen verification snapshot could not be loaded.');
|
|
24
|
+
const snapshot = await response.json();
|
|
25
|
+
const authority = structuredClone(snapshot.descriptor.space);
|
|
26
|
+
const runFixture = async (call: Call, input: unknown) => {
|
|
27
|
+
state.runtimeCalls.push(call);
|
|
28
|
+
try {
|
|
29
|
+
const result = await fetch('/fixture', { method: 'POST', credentials: 'omit',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), signal: AbortSignal.timeout(5_000) });
|
|
31
|
+
const payload = await result.json();
|
|
32
|
+
if (!result.ok) throw new Error(payload.message || 'No matching synthetic fixture.');
|
|
33
|
+
call.ok = true; return payload.result;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
call.ok = false; call.error = error instanceof Error ? error.message : 'Verification request failed.';
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const protocolFailure = (message: string): Promise<never> => { diagnostic('protocol', message); return Promise.reject(new Error(message)); };
|
|
40
|
+
const deny = () => protocolFailure('Use a declared Space action.');
|
|
41
|
+
const queue = createPrefetchQueue(2);
|
|
42
|
+
const runtime: NotisRuntime = {
|
|
43
|
+
...snapshot.descriptor, databases: [],
|
|
44
|
+
context: { ...snapshot.descriptor.context,
|
|
45
|
+
resourceId: snapshot.descriptor.context?.resource_id ?? snapshot.descriptor.context?.resourceId ?? null,
|
|
46
|
+
collectionItem: snapshot.descriptor.context?.collection_item ?? snapshot.descriptor.context?.collectionItem ?? null },
|
|
47
|
+
queryClient: createQueryClient({ maxEntries: 100, schedule: queue }),
|
|
48
|
+
listTools: deny, callTool: deny, request: deny,
|
|
49
|
+
callAction<T>(id: string, inputs: Record<string, unknown> = {}, options: SpaceActionOptions = {}): Promise<T> {
|
|
50
|
+
if (!Object.hasOwn(authority.actions, id)) return protocolFailure('Undeclared verification action.');
|
|
51
|
+
if ((options.readOnly || options.dedupe) && authority.actions[id].readOnly !== true) {
|
|
52
|
+
return protocolFailure('A write action cannot run as a cached read.');
|
|
53
|
+
}
|
|
54
|
+
return runFixture({ op: 'callAction', actionId: id, ok: null }, { action_id: id, inputs });
|
|
55
|
+
},
|
|
56
|
+
viewerRead: authority.viewerReads?.length ? <T,>(operation: SpaceViewerReadOperation, input: Record<string, unknown> = {}): Promise<T> => {
|
|
57
|
+
const family = Object.hasOwn(SPACE_VIEWER_READ_OPERATIONS, operation) ? SPACE_VIEWER_READ_OPERATIONS[operation] : null;
|
|
58
|
+
if (!family || !authority.viewerReads.includes(family)) return protocolFailure('Undeclared verification viewer read.');
|
|
59
|
+
return runFixture({ op: 'viewerRead', actionId: operation, ok: null }, { viewer_read: { operation, input } });
|
|
60
|
+
} : undefined,
|
|
61
|
+
shown: Object.values(authority.shows || {}).some(list => 'database' in list) ? <T,>(name: string, params: Record<string, unknown> = {}): Promise<T> => {
|
|
62
|
+
const list = authority.shows && Object.hasOwn(authority.shows, name) ? authority.shows[name] : null;
|
|
63
|
+
if (!list || !('database' in list)) return protocolFailure('Undeclared verification list.');
|
|
64
|
+
return runFixture({ op: 'shown', actionId: name, ok: null }, { shown: { name, params } });
|
|
65
|
+
} : undefined,
|
|
66
|
+
documentBody(request: SpaceDocumentBodyRequest) {
|
|
67
|
+
return runFixture({ op: 'documentBody', actionId: request?.readAction, ok: null }, { document_body: request });
|
|
68
|
+
},
|
|
69
|
+
navigate(payload) {
|
|
70
|
+
if (payload.kind !== 'space-reference' || typeof payload.alias !== 'string' || !authority.navigation?.includes(payload.alias)) {
|
|
71
|
+
return protocolFailure('Offline navigation requires a declared synthetic destination.');
|
|
72
|
+
}
|
|
73
|
+
// This observes a declaration only; no live navigation or access is claimed.
|
|
74
|
+
state.runtimeCalls.push({ op: 'navigate', actionId: payload.alias, ok: true });
|
|
75
|
+
},
|
|
76
|
+
publishActiveResource() {}, captureContextSelection() {},
|
|
77
|
+
addContext: async () => false, updateContext: async () => false, removeContext: async () => false,
|
|
78
|
+
registerTopBarSearch() {}, setTopBarSearchValue() {}, setTopBarSearchLoading() {},
|
|
79
|
+
};
|
|
80
|
+
const module = await importBundleSource(snapshot.source);
|
|
81
|
+
const view = resolveBundleRouteExport(module, ['SpaceView'], true);
|
|
82
|
+
if (!view) throw new Error('The SpaceView export is missing from this presentation.');
|
|
83
|
+
const host = document.createElement('div'); host.style.cssText = 'min-height:100vh;height:100vh';
|
|
84
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
85
|
+
const css = document.createElement('style'); css.textContent = snapshot.css; shadow.appendChild(css);
|
|
86
|
+
const mount = document.createElement('div'); mount.setAttribute('data-notis-app-mount', 'true');
|
|
87
|
+
mount.style.cssText = 'min-height:0;height:100%'; shadow.appendChild(mount);
|
|
88
|
+
document.body.appendChild(host);
|
|
89
|
+
const controller = document.createElement('div'); document.body.appendChild(controller);
|
|
90
|
+
const mode = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
|
|
91
|
+
document.documentElement.classList.toggle('dark', mode === 'dark');
|
|
92
|
+
state.renderStarted = true;
|
|
93
|
+
ReactDOMClient.createRoot(controller).render(React.createElement(SdkPresentation, {
|
|
94
|
+
mount, runtime, component: view.component, shell: module.__AppShell, theme: { mode, style: { colorScheme: mode } },
|
|
95
|
+
resetKey: snapshot.cacheKey,
|
|
96
|
+
onCommit: (hasContent: boolean) => { state.mounted = hasContent; },
|
|
97
|
+
onError: (error: Error) => { state.mounted = false; state.errors.push({ phase: 'render', message: error.message }); },
|
|
98
|
+
onProtocolError: (error: Error) => diagnostic('protocol', error),
|
|
99
|
+
fallback: (error: Error) => React.createElement('div', { role: 'alert' }, error.message),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
void start().catch(reason => state.errors.push({ phase: 'boot', message: reason instanceof Error ? reason.message : 'Verification did not start.' }));
|