@dinoxx/dinox-cli 1.0.22 → 1.1.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 +33 -10
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +55 -14
- package/dist/cli.js +36 -2
- package/dist/commands/auth/index.d.ts +35 -0
- package/dist/commands/auth/index.js +382 -83
- package/dist/commands/boxes/index.js +2 -6
- package/dist/commands/boxes/model.d.ts +3 -0
- package/dist/commands/boxes/model.js +16 -1
- package/dist/commands/boxes/operations.js +18 -8
- package/dist/commands/boxes/repo.js +13 -11
- package/dist/commands/boxes/store.js +1 -1
- package/dist/commands/config/index.js +7 -5
- package/dist/commands/descriptor/register.js +9 -0
- package/dist/commands/doctor.d.ts +123 -0
- package/dist/commands/doctor.js +539 -0
- package/dist/commands/graph/index.d.ts +1 -1
- package/dist/commands/graph/index.js +35 -21
- package/dist/commands/graph/repo.js +92 -17
- package/dist/commands/meta/index.js +4 -4
- package/dist/commands/meta/repo.js +4 -3
- package/dist/commands/notes/actions.d.ts +1 -1
- package/dist/commands/notes/actions.js +121 -78
- package/dist/commands/notes/bulk.d.ts +1 -0
- package/dist/commands/notes/bulk.js +23 -17
- package/dist/commands/notes/contentEngine.js +69 -11
- package/dist/commands/notes/contentPatch.d.ts +4 -1
- package/dist/commands/notes/contentPatch.js +105 -41
- package/dist/commands/notes/contentPatchCommand.js +28 -11
- package/dist/commands/notes/descriptors.js +50 -20
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +4 -7
- package/dist/commands/notes/export.d.ts +6 -0
- package/dist/commands/notes/export.js +26 -6
- package/dist/commands/notes/metadataMutation.d.ts +1 -0
- package/dist/commands/notes/metadataMutation.js +14 -3
- package/dist/commands/notes/repo.d.ts +46 -4
- package/dist/commands/notes/repo.js +113 -28
- package/dist/commands/notes/runtime.d.ts +2 -1
- package/dist/commands/notes/runtime.js +17 -1
- package/dist/commands/notes/searchSql.js +13 -6
- package/dist/commands/notes/writeReceipt.d.ts +20 -0
- package/dist/commands/notes/writeReceipt.js +82 -0
- package/dist/commands/prompt/index.js +3 -3
- package/dist/commands/prompt/repo.d.ts +4 -1
- package/dist/commands/prompt/repo.js +17 -7
- package/dist/commands/runtime.d.ts +14 -2
- package/dist/commands/runtime.js +89 -19
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/storage/index.js +8 -2
- package/dist/commands/storage/repo.d.ts +4 -0
- package/dist/commands/storage/repo.js +356 -92
- package/dist/commands/sync.d.ts +23 -1
- package/dist/commands/sync.js +105 -60
- package/dist/commands/tags/content.js +9 -0
- package/dist/commands/tags/index.js +2 -6
- package/dist/commands/tags/operations.js +15 -10
- package/dist/commands/tags/repo.d.ts +2 -0
- package/dist/commands/tags/repo.js +18 -5
- package/dist/commands/tags/store.js +1 -1
- package/dist/commands/todo/contentModel.d.ts +4 -3
- package/dist/commands/todo/contentModel.js +78 -28
- package/dist/commands/todo/dryRun.d.ts +1 -0
- package/dist/commands/todo/dryRun.js +17 -13
- package/dist/commands/todo/index.d.ts +6 -0
- package/dist/commands/todo/index.js +89 -123
- package/dist/commands/todo/repo.d.ts +36 -1
- package/dist/commands/todo/repo.js +146 -37
- package/dist/commands/todo/taskIdentity.d.ts +2 -0
- package/dist/commands/todo/taskIdentity.js +64 -0
- package/dist/commands/todo/taskModel.js +27 -17
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +135 -23
- package/dist/commands/writeDryRun.d.ts +3 -2
- package/dist/commands/writeDryRun.js +4 -3
- package/dist/commands/writeDurability.d.ts +5 -0
- package/dist/commands/writeDurability.js +22 -0
- package/dist/config/paths.js +6 -1
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +23 -9
- package/dist/config/store.d.ts +3 -0
- package/dist/config/store.js +61 -14
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/client.d.ts +26 -1
- package/dist/daemon/client.js +236 -12
- package/dist/daemon/ensure.js +74 -12
- package/dist/daemon/index.d.ts +42 -0
- package/dist/daemon/index.js +202 -52
- package/dist/daemon/lifecycle.d.ts +44 -0
- package/dist/daemon/lifecycle.js +118 -0
- package/dist/daemon/pid.d.ts +19 -6
- package/dist/daemon/pid.js +147 -13
- package/dist/daemon/route.d.ts +14 -0
- package/dist/daemon/route.js +40 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +201 -50
- package/dist/daemon/types.d.ts +29 -1
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +25 -1
- package/dist/powersync/connector.js +31 -16
- package/dist/powersync/exclusiveRuntime.d.ts +20 -0
- package/dist/powersync/exclusiveRuntime.js +48 -0
- package/dist/powersync/runtime.d.ts +25 -4
- package/dist/powersync/runtime.js +255 -35
- package/dist/powersync/syncGate.d.ts +4 -1
- package/dist/powersync/syncGate.js +11 -2
- package/dist/powersync/syncPass.d.ts +2 -0
- package/dist/powersync/syncPass.js +15 -4
- package/dist/powersync/tokenIndex.d.ts +13 -0
- package/dist/powersync/tokenIndex.js +216 -37
- package/dist/powersync/uploader.js +107 -52
- package/dist/utils/abort.js +0 -4
- package/dist/utils/asyncRelease.d.ts +2 -0
- package/dist/utils/asyncRelease.js +25 -0
- package/dist/utils/bidlinkCanonicalization.js +3 -0
- package/dist/utils/dryRun.d.ts +1 -1
- package/dist/utils/errors.d.ts +15 -1
- package/dist/utils/errors.js +223 -5
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +51 -15
- package/dist/utils/redact.js +1 -1
- package/dist/utils/sqliteLease.d.ts +8 -0
- package/dist/utils/sqliteLease.js +59 -0
- package/dist/utils/syncTimeout.d.ts +7 -1
- package/dist/utils/syncTimeout.js +8 -7
- package/dist/utils/terminalSanitization.js +6 -2
- package/dist/utils/tiptapMarkdown.js +49 -3
- package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
- package/dist/utils/tiptapMarkdownSyntax.js +249 -0
- package/dist/utils/tokenize.js +24 -2
- package/dist/utils/updateNotice.js +60 -12
- package/package.json +14 -10
- package/dist/commands/mcp/auth.d.ts +0 -4
- package/dist/commands/mcp/auth.js +0 -75
- package/dist/commands/mcp/index.d.ts +0 -2
- package/dist/commands/mcp/index.js +0 -86
- package/dist/commands/mcp/noteContentTools.d.ts +0 -222
- package/dist/commands/mcp/noteContentTools.js +0 -390
- package/dist/commands/mcp/server.d.ts +0 -34
- package/dist/commands/mcp/server.js +0 -424
- package/dist/commands/mcp/tools.d.ts +0 -2
- package/dist/commands/mcp/tools.js +0 -32
- package/dist/commands/mcp/types.d.ts +0 -39
- package/dist/commands/mcp/types.js +0 -1
- package/dist/utils/noteContentEngine.d.ts +0 -333
- package/dist/utils/noteContentEngine.js +0 -740
|
@@ -1,27 +1,239 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { buildPendingAuthConfig, fetchUserInfoWithAuthorization, hydrateResolvedAuthIdentity, mergeAuthIdentityIntoConfig, } from '../../auth/userInfo.js';
|
|
2
|
+
import { buildPendingAuthConfig, fetchUserInfoWithAuthorization, hydrateResolvedAuthIdentity, mergeAuthIdentityIntoConfig, persistedAuthStateMatches, rotatePersistedAuthStateRevision, } from '../../auth/userInfo.js';
|
|
3
3
|
import { getLegacyPowerSyncDbPath, getPowerSyncDbPath } from '../../config/paths.js';
|
|
4
|
-
import { loadConfig,
|
|
4
|
+
import { loadConfig, updateConfig } from '../../config/store.js';
|
|
5
5
|
import { resolveConfig } from '../../config/resolve.js';
|
|
6
|
+
import { getDaemonInfo } from '../../daemon/client.js';
|
|
7
|
+
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
|
|
8
|
+
import { stopDaemonLocked } from '../../daemon/index.js';
|
|
9
|
+
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
6
10
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
7
|
-
import {
|
|
11
|
+
import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
|
|
12
|
+
import { acquirePowerSyncOwnerLease, closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, } from '../../powersync/runtime.js';
|
|
8
13
|
import { runSingleSyncPass } from '../../powersync/syncPass.js';
|
|
9
14
|
import { registerCommandGroup } from '../descriptor/register.js';
|
|
10
15
|
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
11
16
|
import { normalizeAuthToken, readEnvAuthToken } from '../../utils/authToken.js';
|
|
12
17
|
import { DinoxError } from '../../utils/errors.js';
|
|
13
|
-
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
18
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanReadableWarning, printSuccess, } from '../../utils/output.js';
|
|
14
19
|
import { redactAuthorization } from '../../utils/redact.js';
|
|
20
|
+
import { withAsyncRelease } from '../../utils/asyncRelease.js';
|
|
15
21
|
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
16
22
|
const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
17
23
|
const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
24
|
+
const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
|
|
25
|
+
async function withDaemonStartLock(run) {
|
|
26
|
+
return withAsyncRelease(() => acquireDaemonStartLock(), run);
|
|
27
|
+
}
|
|
18
28
|
async function removeSqliteArtifacts(dbPath) {
|
|
19
|
-
for (const suffix of ['', '-wal', '-shm']) {
|
|
20
|
-
await fs.rm(`${dbPath}${suffix}`, { force: true })
|
|
29
|
+
for (const suffix of ['', '-wal', '-shm', '-journal']) {
|
|
30
|
+
await fs.rm(`${dbPath}${suffix}`, { force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createPartialLogoutError(input) {
|
|
34
|
+
const original = input.failure.error instanceof DinoxError ? input.failure.error : null;
|
|
35
|
+
return new DinoxError('Saved credentials were cleared, but logout cleanup could not be completed safely.', {
|
|
36
|
+
code: original?.code ?? 'CACHE_CLEANUP_FAILED',
|
|
37
|
+
cause: input.failure.error,
|
|
38
|
+
details: {
|
|
39
|
+
persistedCredentialsCleared: true,
|
|
40
|
+
environmentTokenActive: input.environmentTokenActive,
|
|
41
|
+
cleanupPhase: input.failure.phase,
|
|
42
|
+
cachePaths: [...input.paths],
|
|
43
|
+
},
|
|
44
|
+
suggestion: original?.suggestion
|
|
45
|
+
?? 'Stop other Dinox processes, then retry `dino auth logout --clear-local-db`.',
|
|
46
|
+
recoverable: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function createOwnerLeaseReleaseError(input) {
|
|
50
|
+
const cause = input.errors.length === 1
|
|
51
|
+
? input.errors[0]
|
|
52
|
+
: new AggregateError(input.errors, 'Multiple PowerSync owner leases failed to release.');
|
|
53
|
+
return new DinoxError('Saved credentials were cleared, but local database ownership could not be released cleanly.', {
|
|
54
|
+
code: 'CACHE_CLEANUP_FAILED',
|
|
55
|
+
cause,
|
|
56
|
+
details: {
|
|
57
|
+
persistedCredentialsCleared: true,
|
|
58
|
+
environmentTokenActive: input.environmentTokenActive,
|
|
59
|
+
cleanupPhase: 'owner-release',
|
|
60
|
+
cachePaths: [...input.paths],
|
|
61
|
+
releaseErrorCount: input.errors.length,
|
|
62
|
+
},
|
|
63
|
+
suggestion: 'Exit other Dinox processes, then retry the command before opening the affected local database again.',
|
|
64
|
+
recoverable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function releaseOwnerLeases(releases) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
for (const release of [...releases].reverse()) {
|
|
70
|
+
try {
|
|
71
|
+
await release();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
errors.push(error);
|
|
75
|
+
}
|
|
21
76
|
}
|
|
77
|
+
return errors;
|
|
78
|
+
}
|
|
79
|
+
async function readAuthTokenFromStdin() {
|
|
80
|
+
if (process.stdin.isTTY) {
|
|
81
|
+
throw new DinoxError('--token-stdin requires piped stdin', {
|
|
82
|
+
code: 'USAGE_ERROR',
|
|
83
|
+
suggestion: 'Pipe the token from a password manager or secret store into `dino auth login --token-stdin`.',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let totalBytes = 0;
|
|
88
|
+
for await (const chunk of process.stdin) {
|
|
89
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
90
|
+
totalBytes += buffer.length;
|
|
91
|
+
if (totalBytes > MAX_STDIN_TOKEN_BYTES) {
|
|
92
|
+
throw new DinoxError('Token from stdin is too large');
|
|
93
|
+
}
|
|
94
|
+
chunks.push(buffer);
|
|
95
|
+
}
|
|
96
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
97
|
+
}
|
|
98
|
+
const authLoginDependencies = {
|
|
99
|
+
acquireDaemonStartLock,
|
|
100
|
+
loadConfig,
|
|
101
|
+
resolveConfig,
|
|
102
|
+
fetchCredentials: fetchCredentialsWithAuthorization,
|
|
103
|
+
fetchUserInfo: fetchUserInfoWithAuthorization,
|
|
104
|
+
suspendDaemon: suspendDaemonForExclusiveAccessLocked,
|
|
105
|
+
connectPowerSync,
|
|
106
|
+
runSingleSyncPass,
|
|
107
|
+
getStatusSnapshot,
|
|
108
|
+
closePowerSyncAndReleaseOwner,
|
|
109
|
+
updateConfig,
|
|
110
|
+
restoreDaemon: restoreSuspendedDaemonLocked,
|
|
111
|
+
printWarning: printHumanReadableWarning,
|
|
112
|
+
};
|
|
113
|
+
function authLoginErrorMessage(error) {
|
|
114
|
+
return error instanceof Error ? error.message : String(error);
|
|
115
|
+
}
|
|
116
|
+
export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
117
|
+
const dependencies = { ...authLoginDependencies, ...overrides };
|
|
118
|
+
const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
|
|
119
|
+
let previousConfig = null;
|
|
120
|
+
let suspended = null;
|
|
121
|
+
let restoreAllowed = true;
|
|
122
|
+
let lifecycleLockReleaseError;
|
|
123
|
+
let outcome;
|
|
124
|
+
try {
|
|
125
|
+
previousConfig = await dependencies.loadConfig();
|
|
126
|
+
const previousResolved = dependencies.resolveConfig(previousConfig);
|
|
127
|
+
const pendingConfig = buildPendingAuthConfig(previousConfig, rawToken);
|
|
128
|
+
const resolved = dependencies.resolveConfig(pendingConfig);
|
|
129
|
+
const timeoutMs = parseSyncTimeoutMs(syncTimeout) ?? resolved.sync.timeoutMs;
|
|
130
|
+
await dependencies.fetchCredentials({
|
|
131
|
+
authorization: rawToken,
|
|
132
|
+
endpoint: resolved.powersync.endpoint,
|
|
133
|
+
credentialsUrl: resolved.powersync.tokenEndpoint,
|
|
134
|
+
timeoutMs,
|
|
135
|
+
});
|
|
136
|
+
const identity = await dependencies.fetchUserInfo({
|
|
137
|
+
authorization: rawToken,
|
|
138
|
+
config: resolved,
|
|
139
|
+
timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
|
|
142
|
+
const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
|
|
143
|
+
suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
|
|
144
|
+
const { db, dbPath, stale, releaseOwner } = await dependencies.connectPowerSync({
|
|
145
|
+
config: verifiedResolved,
|
|
146
|
+
offline: false,
|
|
147
|
+
timeoutMs,
|
|
148
|
+
});
|
|
149
|
+
let syncOutcome;
|
|
150
|
+
try {
|
|
151
|
+
await dependencies.runSingleSyncPass(db, timeoutMs);
|
|
152
|
+
syncOutcome = {
|
|
153
|
+
ok: true,
|
|
154
|
+
lastSyncedAt: dependencies.getStatusSnapshot(db).lastSyncedAt,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
syncOutcome = { ok: false, error };
|
|
159
|
+
}
|
|
160
|
+
let cleanupError;
|
|
161
|
+
try {
|
|
162
|
+
await dependencies.closePowerSyncAndReleaseOwner(db, releaseOwner);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
cleanupError = error;
|
|
166
|
+
restoreAllowed = false;
|
|
167
|
+
dependencies.printWarning(`warning: failed to close the login verification runtime safely: ${authLoginErrorMessage(error)}`);
|
|
168
|
+
}
|
|
169
|
+
if (!syncOutcome.ok) {
|
|
170
|
+
throw syncOutcome.error;
|
|
171
|
+
}
|
|
172
|
+
if (cleanupError) {
|
|
173
|
+
throw cleanupError;
|
|
174
|
+
}
|
|
175
|
+
await dependencies.updateConfig((current) => {
|
|
176
|
+
if (!persistedAuthStateMatches(current, previousConfig)) {
|
|
177
|
+
throw new DinoxError('Authentication changed while login was being verified.', {
|
|
178
|
+
code: 'AUTH_STATE_CHANGED',
|
|
179
|
+
suggestion: 'Retry `dino auth login` if you still want to replace the current login.',
|
|
180
|
+
recoverable: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
|
|
184
|
+
}, { timeoutMs });
|
|
185
|
+
outcome = {
|
|
186
|
+
ok: true,
|
|
187
|
+
value: {
|
|
188
|
+
ok: true,
|
|
189
|
+
verified: true,
|
|
190
|
+
endpoint: verifiedResolved.powersync.endpoint,
|
|
191
|
+
dbPath,
|
|
192
|
+
stale,
|
|
193
|
+
lastSyncedAt: syncOutcome.lastSyncedAt,
|
|
194
|
+
userId: identity.userId,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
outcome = { ok: false, error };
|
|
200
|
+
if (previousConfig && suspended && restoreAllowed) {
|
|
201
|
+
try {
|
|
202
|
+
const current = await dependencies.loadConfig();
|
|
203
|
+
if (persistedAuthStateMatches(current, previousConfig)) {
|
|
204
|
+
await dependencies.restoreDaemon(suspended);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (restoreError) {
|
|
208
|
+
dependencies.printWarning(`warning: failed to restore daemon after login failure: ${authLoginErrorMessage(restoreError)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
try {
|
|
214
|
+
await releaseStartLock();
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
lifecycleLockReleaseError = error;
|
|
218
|
+
dependencies.printWarning(`warning: failed to release daemon lifecycle lock: ${authLoginErrorMessage(error)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!outcome.ok) {
|
|
222
|
+
throw outcome.error;
|
|
223
|
+
}
|
|
224
|
+
if (lifecycleLockReleaseError) {
|
|
225
|
+
throw lifecycleLockReleaseError;
|
|
226
|
+
}
|
|
227
|
+
return outcome.value;
|
|
22
228
|
}
|
|
23
229
|
async function runAuthLogin(context) {
|
|
24
|
-
const
|
|
230
|
+
const options = context.options;
|
|
231
|
+
if (context.args.token && options.tokenStdin) {
|
|
232
|
+
throw new DinoxError('Provide the token either as an argument or with --token-stdin, not both', {
|
|
233
|
+
code: 'USAGE_ERROR',
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const token = options.tokenStdin ? await readAuthTokenFromStdin() : context.args.token;
|
|
25
237
|
const rawToken = normalizeAuthToken(token);
|
|
26
238
|
if (!rawToken) {
|
|
27
239
|
throw new DinoxError('Token is required');
|
|
@@ -30,83 +242,172 @@ async function runAuthLogin(context) {
|
|
|
30
242
|
if (envToken && envToken !== rawToken) {
|
|
31
243
|
throw new DinoxError('DINOX_TOKEN is set and differs from the login token. Unset DINOX_TOKEN before running `dino auth login`, or use the same token.');
|
|
32
244
|
}
|
|
33
|
-
const previousConfig = await loadConfig();
|
|
34
|
-
const pendingConfig = buildPendingAuthConfig(previousConfig, rawToken);
|
|
35
|
-
const resolved = resolveConfig(pendingConfig);
|
|
36
|
-
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? resolved.sync.timeoutMs;
|
|
37
245
|
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
38
|
-
await
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
246
|
+
const result = await performAuthLogin(rawToken, context.globals.syncTimeout);
|
|
247
|
+
if (structuredOutput) {
|
|
248
|
+
printSuccess(result);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
printHumanReadableLine('Login verified.');
|
|
252
|
+
printHumanReadableLine(`- endpoint: ${result.endpoint}`);
|
|
253
|
+
printHumanReadableLine(`- db: ${result.dbPath}`);
|
|
254
|
+
printHumanReadableLine(`- userId: ${result.userId}`);
|
|
255
|
+
if (result.lastSyncedAt) {
|
|
256
|
+
printHumanReadableLine(`- lastSyncedAt: ${result.lastSyncedAt}`);
|
|
257
|
+
}
|
|
258
|
+
if (result.stale) {
|
|
259
|
+
printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function runAuthLogoutLocked(options) {
|
|
263
|
+
const config = await loadConfig();
|
|
264
|
+
const resolved = resolveConfig(config);
|
|
265
|
+
const envTokenActive = Boolean(readEnvAuthToken());
|
|
266
|
+
const daemonInfo = await getDaemonInfo();
|
|
267
|
+
let initiallyResolvedUserId = resolved.auth.userId;
|
|
268
|
+
if (!initiallyResolvedUserId
|
|
269
|
+
&& daemonInfo?.authenticated
|
|
270
|
+
&& daemonInfo.tokenFingerprint === (resolved.auth.tokenFingerprint ?? null)) {
|
|
271
|
+
initiallyResolvedUserId = daemonInfo.userId ?? undefined;
|
|
272
|
+
}
|
|
273
|
+
let daemonStopped = false;
|
|
274
|
+
let cleanupFailure = null;
|
|
56
275
|
try {
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
276
|
+
daemonStopped = (await stopDaemonLocked()).stopped;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
cleanupFailure = { error, phase: 'daemon' };
|
|
280
|
+
}
|
|
281
|
+
let paths = new Set();
|
|
282
|
+
const releases = [];
|
|
283
|
+
let operationFailed = false;
|
|
284
|
+
let operationError;
|
|
285
|
+
let result = null;
|
|
286
|
+
try {
|
|
287
|
+
await updateConfig(async (current) => {
|
|
288
|
+
const currentResolved = resolveConfig(current);
|
|
289
|
+
let effectiveUserId = currentResolved.auth.userId;
|
|
290
|
+
if (!effectiveUserId
|
|
291
|
+
&& daemonInfo?.authenticated
|
|
292
|
+
&& daemonInfo.tokenFingerprint === (currentResolved.auth.tokenFingerprint ?? null)) {
|
|
293
|
+
effectiveUserId = daemonInfo.userId ?? undefined;
|
|
294
|
+
}
|
|
295
|
+
if (!effectiveUserId
|
|
296
|
+
&& currentResolved.auth.tokenFingerprint === resolved.auth.tokenFingerprint) {
|
|
297
|
+
effectiveUserId = initiallyResolvedUserId;
|
|
298
|
+
}
|
|
299
|
+
if (options.clearLocalDb && currentResolved.auth.authorization && !effectiveUserId && !cleanupFailure) {
|
|
300
|
+
try {
|
|
301
|
+
const hydrated = await hydrateResolvedAuthIdentity(currentResolved, {
|
|
302
|
+
timeoutMs: currentResolved.sync.timeoutMs,
|
|
303
|
+
});
|
|
304
|
+
effectiveUserId = hydrated.config.auth.userId;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
cleanupFailure ??= { error, phase: 'identity' };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
paths = options.clearLocalDb
|
|
311
|
+
? new Set([
|
|
312
|
+
getPowerSyncDbPath(current.auth?.userId),
|
|
313
|
+
getPowerSyncDbPath(effectiveUserId),
|
|
314
|
+
getLegacyPowerSyncDbPath(),
|
|
315
|
+
].sort())
|
|
316
|
+
: new Set();
|
|
317
|
+
if (!cleanupFailure) {
|
|
318
|
+
for (const dbPath of paths) {
|
|
319
|
+
try {
|
|
320
|
+
releases.push(await acquirePowerSyncOwnerLease(dbPath, currentResolved.sync.timeoutMs));
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
cleanupFailure = { error, phase: 'owner-lease' };
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return rotatePersistedAuthStateRevision({
|
|
329
|
+
...current,
|
|
330
|
+
auth: {
|
|
331
|
+
...(current.auth ?? {}),
|
|
332
|
+
authorization: undefined,
|
|
333
|
+
userId: undefined,
|
|
334
|
+
username: undefined,
|
|
335
|
+
email: undefined,
|
|
336
|
+
userInfoSyncedAt: undefined,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}, { timeoutMs: resolved.sync.timeoutMs });
|
|
340
|
+
if (cleanupFailure) {
|
|
341
|
+
throw createPartialLogoutError({
|
|
342
|
+
failure: cleanupFailure,
|
|
343
|
+
environmentTokenActive: envTokenActive,
|
|
344
|
+
paths,
|
|
345
|
+
});
|
|
72
346
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
347
|
+
for (const dbPath of paths) {
|
|
348
|
+
try {
|
|
349
|
+
await removeSqliteArtifacts(dbPath);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
throw new DinoxError('Saved credentials were cleared, but the local cache could not be fully removed.', {
|
|
353
|
+
code: 'CACHE_CLEANUP_FAILED',
|
|
354
|
+
cause: error,
|
|
355
|
+
details: {
|
|
356
|
+
persistedCredentialsCleared: true,
|
|
357
|
+
environmentTokenActive: envTokenActive,
|
|
358
|
+
cachePath: dbPath,
|
|
359
|
+
},
|
|
360
|
+
suggestion: `Stop Dinox processes, inspect filesystem permissions, and remove the reported cache path: ${dbPath}`,
|
|
361
|
+
recoverable: true,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
79
364
|
}
|
|
80
|
-
|
|
81
|
-
|
|
365
|
+
result = {
|
|
366
|
+
ok: true,
|
|
367
|
+
loggedOut: !envTokenActive,
|
|
368
|
+
persistedCredentialsCleared: true,
|
|
369
|
+
environmentTokenActive: envTokenActive,
|
|
370
|
+
daemonStopped,
|
|
371
|
+
clearedLocalDb: Boolean(options.clearLocalDb),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
operationFailed = true;
|
|
376
|
+
operationError = error;
|
|
377
|
+
}
|
|
378
|
+
const releaseErrors = await releaseOwnerLeases(releases);
|
|
379
|
+
if (operationFailed) {
|
|
380
|
+
if (releaseErrors.length > 0) {
|
|
381
|
+
printHumanReadableWarning(`warning: logout also failed to release ${releaseErrors.length} local database owner lease(s); the primary error was preserved.`);
|
|
82
382
|
}
|
|
383
|
+
throw operationError;
|
|
83
384
|
}
|
|
84
|
-
|
|
85
|
-
|
|
385
|
+
if (releaseErrors.length > 0) {
|
|
386
|
+
throw createOwnerLeaseReleaseError({
|
|
387
|
+
errors: releaseErrors,
|
|
388
|
+
environmentTokenActive: envTokenActive,
|
|
389
|
+
paths,
|
|
390
|
+
});
|
|
86
391
|
}
|
|
392
|
+
return result;
|
|
87
393
|
}
|
|
88
394
|
async function runAuthLogout(context) {
|
|
89
395
|
const options = context.options;
|
|
90
|
-
const
|
|
91
|
-
const currentUserId = config.auth?.userId;
|
|
92
|
-
if (config.auth) {
|
|
93
|
-
config.auth.authorization = undefined;
|
|
94
|
-
config.auth.userId = undefined;
|
|
95
|
-
config.auth.username = undefined;
|
|
96
|
-
config.auth.email = undefined;
|
|
97
|
-
config.auth.userInfoSyncedAt = undefined;
|
|
98
|
-
}
|
|
99
|
-
await saveConfig(config);
|
|
100
|
-
if (options.clearLocalDb) {
|
|
101
|
-
await removeSqliteArtifacts(getPowerSyncDbPath(currentUserId));
|
|
102
|
-
await removeSqliteArtifacts(getLegacyPowerSyncDbPath());
|
|
103
|
-
}
|
|
396
|
+
const result = await withDaemonStartLock(() => runAuthLogoutLocked(options));
|
|
104
397
|
if (getRequestedStructuredOutputFormat()) {
|
|
105
|
-
printSuccess(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
398
|
+
printSuccess(result);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
printHumanReadableLine('Saved login credentials cleared.');
|
|
402
|
+
printHumanReadableLine(`- daemonStopped: ${result.daemonStopped ? 'yes' : 'no'}`);
|
|
403
|
+
if (result.clearedLocalDb) {
|
|
404
|
+
printHumanReadableLine('- local cache: cleared');
|
|
405
|
+
}
|
|
406
|
+
if (result.environmentTokenActive) {
|
|
407
|
+
printHumanReadableLine('warning: DINOX_TOKEN is still set, so commands remain authenticated in this environment.');
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
printHumanReadableLine('Logged out.');
|
|
110
411
|
}
|
|
111
412
|
}
|
|
112
413
|
async function runAuthStatus(context) {
|
|
@@ -150,12 +451,12 @@ async function runAuthStatus(context) {
|
|
|
150
451
|
printHumanReadableLine(`powersync.uploadBaseUrl: ${payload.powersync.uploadBaseUrl ?? '(unset)'}`);
|
|
151
452
|
return;
|
|
152
453
|
}
|
|
153
|
-
|
|
454
|
+
await withExclusivePowerSync({
|
|
154
455
|
config: resolved,
|
|
155
456
|
offline: false,
|
|
156
457
|
timeoutMs,
|
|
157
|
-
|
|
158
|
-
|
|
458
|
+
expectedTokenFingerprint: resolved.auth.tokenFingerprint ?? null,
|
|
459
|
+
}, async ({ db, stale }) => {
|
|
159
460
|
const status = getStatusSnapshot(db);
|
|
160
461
|
const payload = { ...base, offline: false, stale, status };
|
|
161
462
|
if (structuredOutput) {
|
|
@@ -175,10 +476,7 @@ async function runAuthStatus(context) {
|
|
|
175
476
|
if (payload.stale) {
|
|
176
477
|
printHumanReadableLine('warning: sync timed out; local cache may be stale');
|
|
177
478
|
}
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
await db.close().catch(() => undefined);
|
|
181
|
-
}
|
|
479
|
+
});
|
|
182
480
|
}
|
|
183
481
|
export const authCommandGroup = {
|
|
184
482
|
name: 'auth',
|
|
@@ -187,7 +485,8 @@ export const authCommandGroup = {
|
|
|
187
485
|
{
|
|
188
486
|
name: 'login',
|
|
189
487
|
description: 'Save login token and verify PowerSync connectivity',
|
|
190
|
-
arguments: [{ syntax: '
|
|
488
|
+
arguments: [{ syntax: '[token]', description: 'Login token (omit when using --token-stdin)' }],
|
|
489
|
+
options: [{ flags: '--token-stdin', description: 'Read the login token from piped stdin instead of argv', type: 'boolean' }],
|
|
191
490
|
schema: {
|
|
192
491
|
path: 'auth.login',
|
|
193
492
|
risk: 'write',
|
|
@@ -198,7 +497,7 @@ export const authCommandGroup = {
|
|
|
198
497
|
summary: 'Login verification result',
|
|
199
498
|
fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId'],
|
|
200
499
|
},
|
|
201
|
-
examples: ['dino auth login
|
|
500
|
+
examples: ['dino auth login --token-stdin --format json'],
|
|
202
501
|
},
|
|
203
502
|
action: runAuthLogin,
|
|
204
503
|
},
|
|
@@ -214,7 +513,7 @@ export const authCommandGroup = {
|
|
|
214
513
|
output: {
|
|
215
514
|
type: 'structured',
|
|
216
515
|
summary: 'Logout acknowledgement',
|
|
217
|
-
fields: ['loggedOut', 'clearedLocalDb'],
|
|
516
|
+
fields: ['loggedOut', 'persistedCredentialsCleared', 'environmentTokenActive', 'daemonStopped', 'clearedLocalDb'],
|
|
218
517
|
},
|
|
219
518
|
examples: ['dino auth logout --format json', 'dino auth logout --clear-local-db --format json'],
|
|
220
519
|
},
|
|
@@ -5,7 +5,7 @@ import { buildBoxAddDryRunPreview, buildBoxMutationDryRunPreview } from '../writ
|
|
|
5
5
|
import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
6
6
|
import { DinoxError } from '../../utils/errors.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
8
|
-
import { commandStale, printStaleCacheWarning,
|
|
8
|
+
import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
|
|
9
9
|
import { addBox, inspectAddBox, listBoxes } from './repo.js';
|
|
10
10
|
import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
|
|
11
11
|
const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
@@ -71,7 +71,6 @@ async function runBoxAdd(context) {
|
|
|
71
71
|
description,
|
|
72
72
|
color,
|
|
73
73
|
});
|
|
74
|
-
printUploadDisabledWarning(runtime);
|
|
75
74
|
await waitForMutationUpload(runtime);
|
|
76
75
|
const payload = { ok: true, stale: commandStale(runtime), ...result };
|
|
77
76
|
if (structuredOutput) {
|
|
@@ -114,7 +113,6 @@ async function runBoxRename(context) {
|
|
|
114
113
|
}));
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
|
-
printUploadDisabledWarning(runtime);
|
|
118
116
|
await waitForMutationUpload(runtime);
|
|
119
117
|
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
120
118
|
});
|
|
@@ -140,7 +138,6 @@ async function runBoxMove(context) {
|
|
|
140
138
|
}));
|
|
141
139
|
return;
|
|
142
140
|
}
|
|
143
|
-
printUploadDisabledWarning(runtime);
|
|
144
141
|
await waitForMutationUpload(runtime);
|
|
145
142
|
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
146
143
|
});
|
|
@@ -166,7 +163,6 @@ async function runBoxMerge(context) {
|
|
|
166
163
|
}));
|
|
167
164
|
return;
|
|
168
165
|
}
|
|
169
|
-
printUploadDisabledWarning(runtime);
|
|
170
166
|
await waitForMutationUpload(runtime);
|
|
171
167
|
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
172
168
|
});
|
|
@@ -182,7 +178,7 @@ async function runBoxCleanup(context) {
|
|
|
182
178
|
offline: runtime.offline,
|
|
183
179
|
command: 'dino box cleanup',
|
|
184
180
|
action: 'cleanup_boxes',
|
|
185
|
-
risk: '
|
|
181
|
+
risk: 'read',
|
|
186
182
|
summary: `Inspect zettel box cleanup candidates (${result.issues.length} issues).`,
|
|
187
183
|
inputs: {},
|
|
188
184
|
targets: result.issues,
|
|
@@ -31,4 +31,7 @@ export declare function trimString(value: string | null | undefined): string;
|
|
|
31
31
|
export declare function toNullableId(value: string | null | undefined): string | null;
|
|
32
32
|
export declare function getLeafName(path: string): string;
|
|
33
33
|
export declare function assertMaxLength(label: string, value: string, maxLength: number): void;
|
|
34
|
+
export declare function assertMaxUtf8ByteLength(label: string, value: string, maxBytes: number): void;
|
|
35
|
+
export declare function assertTextLimits(label: string, value: string, maxLength: number): void;
|
|
36
|
+
export declare function assertBoxPathLimits(path: string, label?: string): void;
|
|
34
37
|
export declare function parseStoredBoxIds(raw: string | null): string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
1
2
|
export const MAX_BOX_NAME_LENGTH = 500;
|
|
2
3
|
export const MAX_BOX_PATH_LENGTH = 2000;
|
|
3
4
|
export const MAX_BOX_DESCRIPTION_LENGTH = 200_000;
|
|
@@ -50,6 +51,21 @@ export function assertMaxLength(label, value, maxLength) {
|
|
|
50
51
|
throw new DinoxError(`${label} must be at most ${maxLength} characters`);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
export function assertMaxUtf8ByteLength(label, value, maxBytes) {
|
|
55
|
+
if (Buffer.byteLength(value, 'utf8') > maxBytes) {
|
|
56
|
+
throw new DinoxError(`${label} must be at most ${maxBytes} UTF-8 bytes`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function assertTextLimits(label, value, maxLength) {
|
|
60
|
+
assertMaxLength(label, value, maxLength);
|
|
61
|
+
assertMaxUtf8ByteLength(label, value, maxLength);
|
|
62
|
+
}
|
|
63
|
+
export function assertBoxPathLimits(path, label = 'Box path') {
|
|
64
|
+
assertTextLimits(label, path, MAX_BOX_PATH_LENGTH);
|
|
65
|
+
for (const segment of path.split('/')) {
|
|
66
|
+
assertTextLimits('Box path segment', segment, MAX_BOX_NAME_LENGTH);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
53
69
|
export function parseStoredBoxIds(raw) {
|
|
54
70
|
if (!raw) {
|
|
55
71
|
return [];
|
|
@@ -75,4 +91,3 @@ export function parseStoredBoxIds(raw) {
|
|
|
75
91
|
return [];
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
|
-
import { DinoxError } from '../../utils/errors.js';
|