dsh-server-connector 0.2.0 → 0.2.2
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 +55 -0
- package/README.md +2 -1
- package/lib/client.js +1720 -1716
- package/lib/registry.js +123 -7
- package/package.json +1 -1
package/lib/registry.js
CHANGED
|
@@ -336,13 +336,92 @@ function normalizeOptionalTimestamp(value, field, errorCode) {
|
|
|
336
336
|
return normalizeTimestamp(value, field, errorCode)
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* The grammar every credential ref must satisfy: a POSIX shell identifier.
|
|
341
|
+
*
|
|
342
|
+
* This is @deepseek-ai/dsh-credentials' own contract (`REF_PATTERN`), not an
|
|
343
|
+
* invention of this plugin. Keeping a local copy is deliberate: a ref this
|
|
344
|
+
* plugin persists is later parsed by that package, and a mismatch is not a
|
|
345
|
+
* local error — it prevents `credentials` from activating, which fails the whole
|
|
346
|
+
* Host startup.
|
|
347
|
+
*/
|
|
348
|
+
const CREDENTIAL_REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The ref this plugin derives for a server's password (doc §6.3).
|
|
352
|
+
*
|
|
353
|
+
* `server-connector:<id>:password` was the old shape and it was illegal: the
|
|
354
|
+
* colon and the dash fall outside the grammar above, so persisting it broke the
|
|
355
|
+
* next startup. The id is folded into a single identifier instead
|
|
356
|
+
* (`DSH_SERVER_CONNECTOR_SRV_01_PASSWORD`).
|
|
357
|
+
* @param {string} serverId - e.g. `srv-01`
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
function deriveCredentialRef(serverId) {
|
|
361
|
+
const normalised = String(serverId)
|
|
362
|
+
.toUpperCase()
|
|
363
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
364
|
+
.replace(/^_+|_+$/g, '')
|
|
365
|
+
const safe = normalised === '' ? 'SERVER' : normalised
|
|
366
|
+
return `DSH_SERVER_CONNECTOR_${safe}_PASSWORD`
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate a caller-supplied credential ref against the same grammar.
|
|
371
|
+
*
|
|
372
|
+
* A caller may pass its own ref (an environment-variable name, typically), but
|
|
373
|
+
* an illegal one must be refused here rather than stored: storing it would
|
|
374
|
+
* break the next startup instead of this request.
|
|
375
|
+
* @param {unknown} value
|
|
376
|
+
* @param {RegistryErrorCode} errorCode
|
|
377
|
+
* @returns {string}
|
|
378
|
+
*/
|
|
379
|
+
function assertCredentialRef(value, errorCode) {
|
|
380
|
+
const ref = requiredString(value, 'auth.credentialRef', errorCode)
|
|
381
|
+
if (!CREDENTIAL_REF_PATTERN.test(ref)) {
|
|
382
|
+
throw new RegistryError(
|
|
383
|
+
errorCode,
|
|
384
|
+
`auth.credentialRef ${JSON.stringify(ref)} must be a POSIX identifier ` +
|
|
385
|
+
`(letters, digits and underscores, not starting with a digit), because DSH credentials rejects ` +
|
|
386
|
+
`any other ref and the rejection fails the whole startup.`,
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
return ref
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Read-time recovery for a ref an older build persisted (doc §6.3).
|
|
394
|
+
*
|
|
395
|
+
* Before 0.2.1 this plugin derived `server-connector:<id>:password`, which is
|
|
396
|
+
* outside the grammar. Those records exist on disk in the wild, and refusing
|
|
397
|
+
* them at load would leave the plugin unable to start — trading a Host crash for
|
|
398
|
+
* a plugin that never loads. Instead the illegal legacy shape is mapped onto the
|
|
399
|
+
* derived one, so the record keeps working and the next write persists the fixed
|
|
400
|
+
* ref.
|
|
401
|
+
*
|
|
402
|
+
* Only the exact legacy shape is migrated. Any other illegal ref is still
|
|
403
|
+
* refused: silently rewriting a ref this plugin never wrote could point at a
|
|
404
|
+
* different secret.
|
|
405
|
+
* @param {unknown} value
|
|
406
|
+
* @param {string} serverId
|
|
407
|
+
* @param {RegistryErrorCode} errorCode
|
|
408
|
+
* @returns {string}
|
|
409
|
+
*/
|
|
410
|
+
function migrateCredentialRef(value, serverId, errorCode) {
|
|
411
|
+
const ref = requiredString(value, 'auth.credentialRef', errorCode)
|
|
412
|
+
if (CREDENTIAL_REF_PATTERN.test(ref)) return ref
|
|
413
|
+
if (ref === `server-connector:${serverId}:password`) return deriveCredentialRef(serverId)
|
|
414
|
+
return assertCredentialRef(ref, errorCode)
|
|
415
|
+
}
|
|
416
|
+
|
|
339
417
|
/**
|
|
340
418
|
* @param {unknown} value
|
|
341
419
|
* @param {string} serverId
|
|
342
420
|
* @param {RegistryErrorCode} errorCode
|
|
421
|
+
* @param {{migrate?: boolean}} [options] - `migrate` is set only by the load path.
|
|
343
422
|
* @returns {{kind:'key', privateKeyPath:string}|{kind:'password', credentialRef:string}|{kind:'agent'}}
|
|
344
423
|
*/
|
|
345
|
-
function normalizeAuth(value, serverId, errorCode) {
|
|
424
|
+
function normalizeAuth(value, serverId, errorCode, options = {}) {
|
|
346
425
|
if (value === undefined) return { kind: 'agent' }
|
|
347
426
|
if (!isPlainObject(value)) throw new RegistryError(errorCode, 'auth must be an object.')
|
|
348
427
|
const kind = value.kind
|
|
@@ -352,12 +431,20 @@ function normalizeAuth(value, serverId, errorCode) {
|
|
|
352
431
|
}
|
|
353
432
|
if (kind === 'password') {
|
|
354
433
|
assertKnownFields(value, AUTH_FIELDS.password, errorCode, 'auth')
|
|
355
|
-
// Doc §6.3: the record keeps a reference, never the password itself.
|
|
356
|
-
//
|
|
434
|
+
// Doc §6.3: the record keeps a reference, never the password itself.
|
|
435
|
+
//
|
|
436
|
+
// The ref MUST be a POSIX shell identifier: @deepseek-ai/dsh-credentials
|
|
437
|
+
// validates every stored ref with /^[A-Za-z_][A-Za-z0-9_]*$/ while loading
|
|
438
|
+
// credentials.yaml, and a ref outside that grammar throws during
|
|
439
|
+
// `credentials` activation. Because `credentials` is a required plugin, that
|
|
440
|
+
// takes the entire Host down at startup — a persisted bad ref bricks DSH
|
|
441
|
+
// until the file is edited by hand. Derive and validate accordingly.
|
|
357
442
|
const credentialRef =
|
|
358
443
|
value.credentialRef === undefined
|
|
359
|
-
?
|
|
360
|
-
:
|
|
444
|
+
? deriveCredentialRef(serverId)
|
|
445
|
+
: options.migrate === true
|
|
446
|
+
? migrateCredentialRef(value.credentialRef, serverId, errorCode)
|
|
447
|
+
: assertCredentialRef(value.credentialRef, errorCode)
|
|
361
448
|
return { kind: 'password', credentialRef }
|
|
362
449
|
}
|
|
363
450
|
if (kind === 'agent') {
|
|
@@ -455,6 +542,7 @@ function normalizeCustomRules(value, errorCode) {
|
|
|
455
542
|
* errorCode: RegistryErrorCode,
|
|
456
543
|
* label: string,
|
|
457
544
|
* createdAt?: string,
|
|
545
|
+
* migrate?: boolean,
|
|
458
546
|
* }} options
|
|
459
547
|
* @returns {ServerRecord}
|
|
460
548
|
*/
|
|
@@ -472,7 +560,7 @@ function normalizeRecord(source, options) {
|
|
|
472
560
|
host: normalizeHost(source.host, errorCode),
|
|
473
561
|
port: normalizePort(source.port, errorCode),
|
|
474
562
|
username: requiredString(source.username, 'username', errorCode),
|
|
475
|
-
auth: normalizeAuth(source.auth, id, errorCode),
|
|
563
|
+
auth: normalizeAuth(source.auth, id, errorCode, { migrate: options.migrate === true }),
|
|
476
564
|
hostKeyMode: normalizeHostKeyMode(source.hostKeyMode, errorCode),
|
|
477
565
|
defaultWorkDir: normalizeWorkDir(source.defaultWorkDir, errorCode),
|
|
478
566
|
enabled: normalizeEnabled(source.enabled, errorCode),
|
|
@@ -786,6 +874,9 @@ export class Registry {
|
|
|
786
874
|
id: recordId,
|
|
787
875
|
errorCode: 'INVALID_REGISTRY',
|
|
788
876
|
label: `servers[${index}]`,
|
|
877
|
+
// Loading is the one path allowed to repair a legacy credential ref;
|
|
878
|
+
// writes are strict so this plugin can never persist a bad one again.
|
|
879
|
+
migrate: true,
|
|
789
880
|
})
|
|
790
881
|
if (seenIds.has(record.id)) {
|
|
791
882
|
throw new RegistryError('INVALID_REGISTRY', `${this.#file}: duplicate server id "${record.id}".`)
|
|
@@ -798,7 +889,32 @@ export class Registry {
|
|
|
798
889
|
seenNames.add(nameKey)
|
|
799
890
|
servers.push(record)
|
|
800
891
|
}
|
|
801
|
-
|
|
892
|
+
const document = { version: REGISTRY_VERSION, servers }
|
|
893
|
+
// Persist a repaired legacy ref immediately. Without this the migration
|
|
894
|
+
// lives only in memory: every later read re-runs it, and a copy of the file
|
|
895
|
+
// taken before the first write would still carry the illegal ref that breaks
|
|
896
|
+
// the next startup. The rewrite is conditional so a clean file is never
|
|
897
|
+
// touched (its mtime stays put).
|
|
898
|
+
if (this.#migrated(text, document)) await this.#save(document)
|
|
899
|
+
return document
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Whether the loaded document differs from what is on disk, i.e. whether the
|
|
904
|
+
* legacy-ref migration actually changed a record and the file must be rewritten.
|
|
905
|
+
* @param {string} text - the raw file text that was parsed.
|
|
906
|
+
* @param {RegistryDocument} document - the normalised document.
|
|
907
|
+
* @returns {boolean}
|
|
908
|
+
*/
|
|
909
|
+
#migrated(text, document) {
|
|
910
|
+
for (const record of document.servers) {
|
|
911
|
+
if (record.auth.kind !== 'password') continue
|
|
912
|
+
const ref = record.auth.credentialRef
|
|
913
|
+
// The normalised ref is legal by construction; if the raw text does not
|
|
914
|
+
// contain it, this record was migrated.
|
|
915
|
+
if (!text.includes(JSON.stringify(ref))) return true
|
|
916
|
+
}
|
|
917
|
+
return false
|
|
802
918
|
}
|
|
803
919
|
|
|
804
920
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-server-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "DSH plugin: SSH execution for servers configured on this machine, with a built-in high-risk command policy (block / confirm / allow).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|