@truenas/api-client 3.0.4 → 3.0.5
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/dist/index.cjs +995 -887
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -44
- package/dist/index.d.ts +109 -44
- package/dist/index.js +995 -887
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -369,775 +369,153 @@ var TrueNasApi = class {
|
|
|
369
369
|
}
|
|
370
370
|
};
|
|
371
371
|
|
|
372
|
-
// src/
|
|
373
|
-
var
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
372
|
+
// src/generated/v25_10_0/index.ts
|
|
373
|
+
var v25_10_0_exports = {};
|
|
374
|
+
__export(v25_10_0_exports, {
|
|
375
|
+
ACLTemplateEntryAcltypeInput: () => ACLTemplateEntryAcltypeInput,
|
|
376
|
+
Aclmode: () => Aclmode,
|
|
377
|
+
Acltype: () => Acltype,
|
|
378
|
+
Action: () => Action,
|
|
379
|
+
Advpowermgmt: () => Advpowermgmt,
|
|
380
|
+
Algorithm: () => Algorithm,
|
|
381
|
+
AppContainerDetailsStateInput: () => AppContainerDetailsStateInput,
|
|
382
|
+
Atime: () => Atime,
|
|
383
|
+
Authenticator: () => Authenticator,
|
|
384
|
+
Authmethod: () => Authmethod,
|
|
385
|
+
Autotrim: () => Autotrim,
|
|
386
|
+
Basic: () => Basic,
|
|
387
|
+
Blocksize: () => Blocksize,
|
|
388
|
+
Bootloader: () => Bootloader,
|
|
389
|
+
Casesensitivity: () => Casesensitivity,
|
|
390
|
+
CloudBackupSnapshotItemType: () => CloudBackupSnapshotItemType,
|
|
391
|
+
CloudSyncCreateDirection: () => CloudSyncCreateDirection,
|
|
392
|
+
CloudSyncCreateDirectionInput: () => CloudSyncCreateDirectionInput,
|
|
393
|
+
CloudTaskAttributesEncryptionInput: () => CloudTaskAttributesEncryptionInput,
|
|
394
|
+
Code: () => Code,
|
|
395
|
+
Compression: () => Compression,
|
|
396
|
+
CorePingRemoteTypeInput: () => CorePingRemoteTypeInput,
|
|
397
|
+
Credentials: () => Credentials,
|
|
398
|
+
DeviceGetInfoOtherTypeInput: () => DeviceGetInfoOtherTypeInput,
|
|
399
|
+
DiskDetailsTypeInput: () => DiskDetailsTypeInput,
|
|
400
|
+
DiskWipeModeInput: () => DiskWipeModeInput,
|
|
401
|
+
DockerStatusInfoStatusInput: () => DockerStatusInfoStatusInput,
|
|
402
|
+
Enclosure2SetSlotStatusStatusInput: () => Enclosure2SetSlotStatusStatusInput,
|
|
403
|
+
Encryption: () => Encryption,
|
|
404
|
+
Endpoint: () => Endpoint,
|
|
405
|
+
Exec: () => Exec,
|
|
406
|
+
Feature: () => Feature,
|
|
407
|
+
FilesystemDirEntryType: () => FilesystemDirEntryType,
|
|
408
|
+
Function: () => Function,
|
|
409
|
+
GetDisplayDeviceType: () => GetDisplayDeviceType,
|
|
410
|
+
Hddstandby: () => Hddstandby,
|
|
411
|
+
InitShutdownScriptCreateType: () => InitShutdownScriptCreateType,
|
|
412
|
+
InitShutdownScriptCreateTypeInput: () => InitShutdownScriptCreateTypeInput,
|
|
413
|
+
InterfaceCreateAliasTypeInput: () => InterfaceCreateAliasTypeInput,
|
|
414
|
+
InterfaceCreateTypeInput: () => InterfaceCreateTypeInput,
|
|
415
|
+
Iotype: () => Iotype,
|
|
416
|
+
IpmiChassisIdentifyVerb: () => IpmiChassisIdentifyVerb,
|
|
417
|
+
IscsiExtentCreateType: () => IscsiExtentCreateType,
|
|
418
|
+
IscsiExtentCreateTypeInput: () => IscsiExtentCreateTypeInput,
|
|
419
|
+
IscsiTargetCreateMode: () => IscsiTargetCreateMode,
|
|
420
|
+
IscsiTargetCreateModeInput: () => IscsiTargetCreateModeInput,
|
|
421
|
+
KeychainCredentialEntryType: () => KeychainCredentialEntryType,
|
|
422
|
+
KeychainCredentialEntryTypeInput: () => KeychainCredentialEntryTypeInput,
|
|
423
|
+
Level: () => Level,
|
|
424
|
+
NFS4ACEType: () => NFS4ACEType,
|
|
425
|
+
NFS4ACETypeInput: () => NFS4ACETypeInput,
|
|
426
|
+
Name: () => Name,
|
|
427
|
+
Policy: () => Policy,
|
|
428
|
+
PoolCreateChecksum: () => PoolCreateChecksum,
|
|
429
|
+
PoolCreateDeduplication: () => PoolCreateDeduplication,
|
|
430
|
+
PoolCreateTopologyDataVdevDRAIDTypeInput: () => PoolCreateTopologyDataVdevDRAIDTypeInput,
|
|
431
|
+
PoolDatasetCreateFilesystemAcltypeInput: () => PoolDatasetCreateFilesystemAcltypeInput,
|
|
432
|
+
PoolDatasetCreateFilesystemChecksum: () => PoolDatasetCreateFilesystemChecksum,
|
|
433
|
+
PoolDatasetCreateFilesystemDeduplication: () => PoolDatasetCreateFilesystemDeduplication,
|
|
434
|
+
PoolDatasetCreateFilesystemReadonlyInput: () => PoolDatasetCreateFilesystemReadonlyInput,
|
|
435
|
+
PoolDatasetDeleteResult: () => PoolDatasetDeleteResult,
|
|
436
|
+
PoolScanStateInput: () => PoolScanStateInput,
|
|
437
|
+
Protocol: () => Protocol,
|
|
438
|
+
Purpose: () => Purpose,
|
|
439
|
+
Readonly: () => Readonly,
|
|
440
|
+
ReplicationCountEligibleManualSnapshotsTransportInput: () => ReplicationCountEligibleManualSnapshotsTransportInput,
|
|
441
|
+
ReplicationCreateReadonlyInput: () => ReplicationCreateReadonlyInput,
|
|
442
|
+
Resolution: () => Resolution,
|
|
443
|
+
Rpm: () => Rpm,
|
|
444
|
+
RsyncTaskCreateDirection: () => RsyncTaskCreateDirection,
|
|
445
|
+
RsyncTaskCreateDirectionInput: () => RsyncTaskCreateDirectionInput,
|
|
446
|
+
RsyncTaskCreateMode: () => RsyncTaskCreateMode,
|
|
447
|
+
RsyncTaskCreateModeInput: () => RsyncTaskCreateModeInput,
|
|
448
|
+
SMBEntryEncryptionInput: () => SMBEntryEncryptionInput,
|
|
449
|
+
STIGType: () => STIGType,
|
|
450
|
+
Schema: () => Schema,
|
|
451
|
+
Security: () => Security,
|
|
452
|
+
Serialspeed: () => Serialspeed,
|
|
453
|
+
ServiceControlVerb: () => ServiceControlVerb,
|
|
454
|
+
SharingSMBEntryPurposeInput: () => SharingSMBEntryPurposeInput,
|
|
455
|
+
SharingSMBEntryPurposeInput2: () => SharingSMBEntryPurposeInput2,
|
|
456
|
+
Shutdown: () => Shutdown,
|
|
457
|
+
Snapdev: () => Snapdev,
|
|
458
|
+
Snapdir: () => Snapdir,
|
|
459
|
+
Source: () => Source,
|
|
460
|
+
SourceInput: () => SourceInput,
|
|
461
|
+
State: () => State,
|
|
462
|
+
Status: () => Status,
|
|
463
|
+
SupportNewTicketCommunityTypeInput: () => SupportNewTicketCommunityTypeInput,
|
|
464
|
+
Sync: () => Sync,
|
|
465
|
+
SyslogServerTransportInput: () => SyslogServerTransportInput,
|
|
466
|
+
Sysloglevel: () => Sysloglevel,
|
|
467
|
+
SystemProductTypeResult: () => SystemProductTypeResult,
|
|
468
|
+
SystemStateResult: () => SystemStateResult,
|
|
469
|
+
Tag: () => Tag,
|
|
470
|
+
Target: () => Target,
|
|
471
|
+
Time: () => Time,
|
|
472
|
+
Transport: () => Transport,
|
|
473
|
+
TruecommandConfigChangedEventFieldsStatus: () => TruecommandConfigChangedEventFieldsStatus,
|
|
474
|
+
TruecommandConfigChangedEventFieldsStatusInput: () => TruecommandConfigChangedEventFieldsStatusInput,
|
|
475
|
+
TunableCreateType: () => TunableCreateType,
|
|
476
|
+
TunableCreateTypeInput: () => TunableCreateTypeInput,
|
|
477
|
+
UPSEntryMode: () => UPSEntryMode,
|
|
478
|
+
UPSEntryModeInput: () => UPSEntryModeInput,
|
|
479
|
+
Unixcharset: () => Unixcharset,
|
|
480
|
+
Username: () => Username,
|
|
481
|
+
VMDiskDeviceType: () => VMDiskDeviceType,
|
|
482
|
+
VMDiskDeviceTypeInput: () => VMDiskDeviceTypeInput,
|
|
483
|
+
VMWareEntryStateStateInput: () => VMWareEntryStateStateInput,
|
|
484
|
+
VMWareMatchDatastoresWithDatasetsResultFilesystemType: () => VMWareMatchDatastoresWithDatasetsResultFilesystemType,
|
|
485
|
+
Vendor: () => Vendor,
|
|
486
|
+
Volblocksize: () => Volblocksize,
|
|
487
|
+
When: () => When,
|
|
488
|
+
ZfsTierRewriteJobEntryStatusInput: () => ZfsTierRewriteJobEntryStatusInput
|
|
489
|
+
});
|
|
380
490
|
|
|
381
|
-
// src/
|
|
382
|
-
var
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return AuthErrorCode2;
|
|
388
|
-
})(AuthErrorCode || {});
|
|
389
|
-
var AuthError = class _AuthError extends Error {
|
|
390
|
-
constructor(code, message) {
|
|
391
|
-
super(message);
|
|
392
|
-
this.code = code;
|
|
393
|
-
this.name = "AuthError";
|
|
394
|
-
Object.setPrototypeOf(this, _AuthError.prototype);
|
|
395
|
-
}
|
|
491
|
+
// src/generated/v25_10_0/api-types.ts
|
|
492
|
+
var Aclmode = {
|
|
493
|
+
Passthrough: "PASSTHROUGH",
|
|
494
|
+
Restricted: "RESTRICTED",
|
|
495
|
+
Discard: "DISCARD",
|
|
496
|
+
Inherit: "INHERIT"
|
|
396
497
|
};
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (response.response_type === "AUTH_ERR" /* AuthErr */) {
|
|
401
|
-
return rxjs.throwError(() => new AuthError(code, message));
|
|
402
|
-
}
|
|
403
|
-
return rxjs.of(response);
|
|
404
|
-
});
|
|
405
|
-
var TrueNasAuthenticator = class _TrueNasAuthenticator {
|
|
406
|
-
constructor(connection) {
|
|
407
|
-
this.connection = connection;
|
|
408
|
-
// in seconds (5 minutes)
|
|
409
|
-
/**
|
|
410
|
-
* whether or not the system is currently authenticated and accessible.
|
|
411
|
-
* this is a `BehaviorSubject` instead of a signal, since we use its `getValue` method
|
|
412
|
-
* throughout a lot of the codebase.
|
|
413
|
-
*/
|
|
414
|
-
this.authenticated$ = new rxjs.BehaviorSubject(false);
|
|
415
|
-
/**
|
|
416
|
-
* `true` when the system is currently being authenticated to, and `false` otherwise.
|
|
417
|
-
* does not indicate whatsoever if authentication is successful or not, just whether the process
|
|
418
|
-
* is ongoing.
|
|
419
|
-
*/
|
|
420
|
-
this.authenticating$ = new rxjs.BehaviorSubject(false);
|
|
421
|
-
this.credentials = { username: "", password: "", key: "" };
|
|
422
|
-
this.sessionLifetime = _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
423
|
-
this.connection.opened.pipe(
|
|
424
|
-
rxjs.filter(
|
|
425
|
-
(isOpen) => !!(isOpen && this.credentials?.username && (this.credentials.password || this.credentials.key))
|
|
426
|
-
),
|
|
427
|
-
rxjs.switchMap(() => {
|
|
428
|
-
if (this.credentials.password) {
|
|
429
|
-
return this.loginWithUserPass(
|
|
430
|
-
this.credentials.username,
|
|
431
|
-
this.credentials.password
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
return this.loginWithApiKey({
|
|
435
|
-
username: this.credentials.username,
|
|
436
|
-
key: this.credentials.key
|
|
437
|
-
});
|
|
438
|
-
})
|
|
439
|
-
).subscribe();
|
|
440
|
-
connection.closed.subscribe(() => {
|
|
441
|
-
this.sessionLifetime = _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
442
|
-
this.authenticated$.next(false);
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
static {
|
|
446
|
-
this.DefaultSessionLifetime = 300;
|
|
447
|
-
}
|
|
448
|
-
loginWithUserPass(username, password) {
|
|
449
|
-
const message = createJsonRpcMessage("auth.login_ex", [
|
|
450
|
-
{
|
|
451
|
-
mechanism: "PASSWORD_PLAIN" /* Password */,
|
|
452
|
-
username,
|
|
453
|
-
password
|
|
454
|
-
}
|
|
455
|
-
]);
|
|
456
|
-
this.authenticating$.next(true);
|
|
457
|
-
this.connection.send(message);
|
|
458
|
-
const messageId = message.id ?? "";
|
|
459
|
-
return this.connection.messages().pipe(
|
|
460
|
-
withId(messageId),
|
|
461
|
-
rxjs.map((msg) => {
|
|
462
|
-
if (msg.error) {
|
|
463
|
-
const errorMessage = getApiErrorMessage(
|
|
464
|
-
msg.error,
|
|
465
|
-
"Authentication failed"
|
|
466
|
-
);
|
|
467
|
-
throw new Error(errorMessage);
|
|
468
|
-
}
|
|
469
|
-
return msg.result;
|
|
470
|
-
}),
|
|
471
|
-
throwOnAuthenticationFailure(
|
|
472
|
-
"PASSWORD_AUTH_FAILED" /* PasswordAuthFailed */,
|
|
473
|
-
"TrueNAS authentication failed. Please verify your TrueNAS user credentials and try again."
|
|
474
|
-
),
|
|
475
|
-
rxjs.tap((res) => {
|
|
476
|
-
if (res.response_type === "SUCCESS" /* Success */) {
|
|
477
|
-
if (res.user_info?.privilege.roles.$set.includes("FULL_ADMIN" /* FullAdmin */)) {
|
|
478
|
-
this.credentials.username = username;
|
|
479
|
-
this.credentials.password = password;
|
|
480
|
-
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
481
|
-
this.authenticated$.next(true);
|
|
482
|
-
} else {
|
|
483
|
-
this.logout();
|
|
484
|
-
this.authenticated$.next(false);
|
|
485
|
-
throw new AuthError(
|
|
486
|
-
"FULL_ADMIN_REQUIRED" /* FullAdminRequired */,
|
|
487
|
-
"User account must have full admin privileges"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}),
|
|
492
|
-
rxjs.finalize(() => {
|
|
493
|
-
this.authenticating$.next(false);
|
|
494
|
-
}),
|
|
495
|
-
rxjs.take(1)
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
loginWithOtp(code) {
|
|
499
|
-
const message = createJsonRpcMessage("auth.login_ex", [
|
|
500
|
-
{
|
|
501
|
-
mechanism: "OTP_TOKEN" /* Otp */,
|
|
502
|
-
otp_token: code
|
|
503
|
-
}
|
|
504
|
-
]);
|
|
505
|
-
this.authenticating$.next(true);
|
|
506
|
-
this.connection.send(message);
|
|
507
|
-
const messageId = message.id ?? "";
|
|
508
|
-
return this.connection.messages().pipe(
|
|
509
|
-
withId(messageId),
|
|
510
|
-
rxjs.map((msg) => {
|
|
511
|
-
if (msg.error) {
|
|
512
|
-
const errorMessage = getApiErrorMessage(
|
|
513
|
-
msg.error,
|
|
514
|
-
"Authentication failed"
|
|
515
|
-
);
|
|
516
|
-
throw new Error(errorMessage);
|
|
517
|
-
}
|
|
518
|
-
return msg.result;
|
|
519
|
-
}),
|
|
520
|
-
rxjs.tap((res) => {
|
|
521
|
-
if (res?.response_type === "SUCCESS" /* Success */) {
|
|
522
|
-
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
523
|
-
this.authenticated$.next(true);
|
|
524
|
-
}
|
|
525
|
-
}),
|
|
526
|
-
throwOnAuthenticationFailure(
|
|
527
|
-
"OTP_AUTH_FAILED" /* OtpAuthFailed */,
|
|
528
|
-
"TrueNAS authentication failed. Please verify your one-time passcode and try again."
|
|
529
|
-
),
|
|
530
|
-
rxjs.finalize(() => {
|
|
531
|
-
this.authenticating$.next(false);
|
|
532
|
-
}),
|
|
533
|
-
rxjs.take(1)
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
loginWithApiKey(credentials) {
|
|
537
|
-
const { username, key } = credentials;
|
|
538
|
-
const message = createJsonRpcMessage("auth.login_ex", [
|
|
539
|
-
{
|
|
540
|
-
mechanism: "API_KEY_PLAIN" /* ApiKey */,
|
|
541
|
-
username,
|
|
542
|
-
api_key: key
|
|
543
|
-
}
|
|
544
|
-
]);
|
|
545
|
-
this.authenticating$.next(true);
|
|
546
|
-
this.connection.send(message);
|
|
547
|
-
const messageId = message.id ?? "";
|
|
548
|
-
return this.connection.messages().pipe(
|
|
549
|
-
withId(messageId),
|
|
550
|
-
rxjs.map((msg) => {
|
|
551
|
-
if (msg.error) {
|
|
552
|
-
const errorMessage = getApiErrorMessage(
|
|
553
|
-
msg.error,
|
|
554
|
-
"Authentication failed"
|
|
555
|
-
);
|
|
556
|
-
throw new Error(errorMessage);
|
|
557
|
-
}
|
|
558
|
-
return msg.result;
|
|
559
|
-
}),
|
|
560
|
-
throwOnAuthenticationFailure(
|
|
561
|
-
"API_KEY_AUTH_FAILED" /* ApiKeyAuthFailed */,
|
|
562
|
-
"TrueNAS authentication failed. Has the TrueNAS Connect API key been removed from your TrueNAS server?"
|
|
563
|
-
),
|
|
564
|
-
rxjs.tap((res) => {
|
|
565
|
-
this.credentials.username = username;
|
|
566
|
-
this.credentials.key = key;
|
|
567
|
-
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
568
|
-
this.authenticated$.next(true);
|
|
569
|
-
}),
|
|
570
|
-
rxjs.finalize(() => {
|
|
571
|
-
this.authenticating$.next(false);
|
|
572
|
-
}),
|
|
573
|
-
rxjs.take(1)
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
newApiKey(username) {
|
|
577
|
-
const message = createJsonRpcMessage("api_key.create", [
|
|
578
|
-
{ name: `tnc-${randomUUID()}`, username }
|
|
579
|
-
]);
|
|
580
|
-
this.connection.send(message);
|
|
581
|
-
const messageId = message.id ?? "";
|
|
582
|
-
return this.connection.messages().pipe(
|
|
583
|
-
withId(messageId),
|
|
584
|
-
rxjs.map((msg) => {
|
|
585
|
-
if (msg.error) {
|
|
586
|
-
const errorMessage = getApiErrorMessage(
|
|
587
|
-
msg.error,
|
|
588
|
-
"Failed to create API key"
|
|
589
|
-
);
|
|
590
|
-
throw new Error(errorMessage);
|
|
591
|
-
}
|
|
592
|
-
return msg.result;
|
|
593
|
-
}),
|
|
594
|
-
rxjs.take(1)
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
logout() {
|
|
598
|
-
const message = createJsonRpcMessage("auth.logout");
|
|
599
|
-
this.connection.send(message);
|
|
600
|
-
const messageId = message.id ?? "";
|
|
601
|
-
return this.connection.messages().pipe(
|
|
602
|
-
withId(messageId),
|
|
603
|
-
rxjs.map((msg) => {
|
|
604
|
-
if (msg.error) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
|
-
return msg.result;
|
|
608
|
-
}),
|
|
609
|
-
rxjs.tap((success) => {
|
|
610
|
-
this.sessionLifetime = _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
611
|
-
this.authenticated$.next(!success);
|
|
612
|
-
}),
|
|
613
|
-
rxjs.take(1)
|
|
614
|
-
);
|
|
615
|
-
}
|
|
498
|
+
var ACLTemplateEntryAcltypeInput = {
|
|
499
|
+
Nfs4: "NFS4",
|
|
500
|
+
Posix1e: "POSIX1E"
|
|
616
501
|
};
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
trace: (message, ...args) => console.trace(message, ...args),
|
|
621
|
-
debug: (message, ...args) => console.debug(message, ...args),
|
|
622
|
-
info: (message, ...args) => console.info(message, ...args),
|
|
623
|
-
warn: (message, ...args) => console.warn(message, ...args),
|
|
624
|
-
error: (message, ...args) => console.error(message, ...args)
|
|
502
|
+
var Acltype = {
|
|
503
|
+
Nfs4: "NFS4",
|
|
504
|
+
Posix1e: "POSIX1E"
|
|
625
505
|
};
|
|
626
|
-
var
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
info: noop,
|
|
631
|
-
warn: noop,
|
|
632
|
-
error: noop
|
|
506
|
+
var Action = {
|
|
507
|
+
Start: "START",
|
|
508
|
+
Stop: "STOP",
|
|
509
|
+
Pause: "PAUSE"
|
|
633
510
|
};
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
if (reason.includes("502")) {
|
|
644
|
-
return "Bad Gateway - System may be starting up";
|
|
645
|
-
}
|
|
646
|
-
if (reason.includes("503")) {
|
|
647
|
-
return "Service Unavailable - System may be overloaded";
|
|
648
|
-
}
|
|
649
|
-
return "";
|
|
650
|
-
}
|
|
651
|
-
function getWebSocketError(code) {
|
|
652
|
-
switch (code) {
|
|
653
|
-
case 1e3:
|
|
654
|
-
return "Connection closed normally";
|
|
655
|
-
case 1001:
|
|
656
|
-
return "Server is shutting down or restarting";
|
|
657
|
-
case 1002:
|
|
658
|
-
return "Protocol error - Invalid WebSocket communication";
|
|
659
|
-
case 1003:
|
|
660
|
-
return "Unsupported data format received";
|
|
661
|
-
case 1006:
|
|
662
|
-
return "Connection lost unexpectedly - Check network connectivity";
|
|
663
|
-
case 1007:
|
|
664
|
-
return "Invalid data received from server";
|
|
665
|
-
case 1008:
|
|
666
|
-
return "Connection terminated due to policy violation";
|
|
667
|
-
case 1009:
|
|
668
|
-
return "Message size exceeded server limits";
|
|
669
|
-
case 1011:
|
|
670
|
-
return "Server encountered an internal error";
|
|
671
|
-
case 1012:
|
|
672
|
-
return "Service is restarting";
|
|
673
|
-
case 1013:
|
|
674
|
-
return "Server temporarily overloaded - Try again later";
|
|
675
|
-
case 1015:
|
|
676
|
-
return "TLS/Certificate error - Certificate may be expired";
|
|
677
|
-
default:
|
|
678
|
-
return "Network connection failed - Check IP address and connectivity";
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
var TrueNasSocket = class {
|
|
682
|
-
constructor(config) {
|
|
683
|
-
this.config = config;
|
|
684
|
-
this.socket = webSocket.webSocket(this.config);
|
|
685
|
-
}
|
|
686
|
-
messages() {
|
|
687
|
-
return this.socket;
|
|
688
|
-
}
|
|
689
|
-
next(msg) {
|
|
690
|
-
this.socket.next(msg);
|
|
691
|
-
}
|
|
692
|
-
complete() {
|
|
693
|
-
this.socket.complete();
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
// src/types/transport.type.ts
|
|
698
|
-
function httpScheme(protocol) {
|
|
699
|
-
return protocol === "http:" ? "http:" : "https:";
|
|
700
|
-
}
|
|
701
|
-
function socketScheme(protocol) {
|
|
702
|
-
return protocol === "http:" ? "ws:" : "wss:";
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// src/connection/truenas-connection.ts
|
|
706
|
-
var tenSeconds = 10 * 1e3;
|
|
707
|
-
var twentySeconds = 20 * 1e3;
|
|
708
|
-
var TrueNasConnection = class {
|
|
709
|
-
constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:") {
|
|
710
|
-
this.hostnames = hostnames;
|
|
711
|
-
this.systemUuid = systemUuid;
|
|
712
|
-
this.websocketPath = websocketPath;
|
|
713
|
-
this.systemName = systemName;
|
|
714
|
-
this.retryDelay = retryDelay;
|
|
715
|
-
this.maxRetry = maxRetry;
|
|
716
|
-
this.logger = logger;
|
|
717
|
-
this.protocol = protocol;
|
|
718
|
-
// compatibility properties
|
|
719
|
-
this.opened = new rxjs.BehaviorSubject(false);
|
|
720
|
-
this.closed = new rxjs.Subject();
|
|
721
|
-
this.hostname = new rxjs.BehaviorSubject("");
|
|
722
|
-
this.connectionAttempts = new rxjs.BehaviorSubject(0);
|
|
723
|
-
this.lastErrorMessage = new rxjs.BehaviorSubject(null);
|
|
724
|
-
/**
|
|
725
|
-
* emits when the connection is manually closed.
|
|
726
|
-
*/
|
|
727
|
-
this.closeConnection = new rxjs.Subject();
|
|
728
|
-
/**
|
|
729
|
-
* the connection only establishes a socket while this gate is `true`. consumers
|
|
730
|
-
* flip it via `setEnabled()` (the app maps `SystemState.Active -> true`).
|
|
731
|
-
*/
|
|
732
|
-
this.enabled$ = new rxjs.BehaviorSubject(false);
|
|
733
|
-
/**
|
|
734
|
-
* observable which emits the current gate value and only emits again when it changes.
|
|
735
|
-
*/
|
|
736
|
-
this.enabledChange$ = this.enabled$.pipe(rxjs.distinctUntilChanged());
|
|
737
|
-
/**
|
|
738
|
-
* observable that handles the entire connection lifecycle.
|
|
739
|
-
*/
|
|
740
|
-
this.connection$ = this.enabledChange$.pipe(
|
|
741
|
-
rxjs.switchMap((enabled) => {
|
|
742
|
-
if (enabled) {
|
|
743
|
-
return this.connect().pipe(
|
|
744
|
-
rxjs.map((conn) => makeActiveConnection(conn.ws, conn.hostname))
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
return rxjs.of(closedConnection);
|
|
748
|
-
}),
|
|
749
|
-
rxjs.switchMap((connection) => {
|
|
750
|
-
if (connection.state !== "active") {
|
|
751
|
-
return rxjs.of(connection);
|
|
752
|
-
}
|
|
753
|
-
const initMessage = createJsonRpcMessage("core.set_options", [
|
|
754
|
-
{ legacy_jobs: false }
|
|
755
|
-
]);
|
|
756
|
-
connection.ws.next(initMessage);
|
|
757
|
-
return connection.ws.messages().pipe(
|
|
758
|
-
rxjs.filter((msg) => msg.id === initMessage.id),
|
|
759
|
-
rxjs.take(1),
|
|
760
|
-
rxjs.map(() => connection)
|
|
761
|
-
);
|
|
762
|
-
}),
|
|
763
|
-
// in the event of an error, do two things:
|
|
764
|
-
// 1. emit an errored connection downstream to inform consumers that
|
|
765
|
-
// the connection is dead.
|
|
766
|
-
// 2. re-throw an error so the downstream `retry` will re-subscribe.
|
|
767
|
-
rxjs.catchError((err) => {
|
|
768
|
-
this.logger.error("All connections failed - retrying", { message: err?.message, hostname: err?.hostname });
|
|
769
|
-
return rxjs.concat(
|
|
770
|
-
rxjs.of(makeConnectionError(err.message, err.hostname)),
|
|
771
|
-
// since this error will immediately get caught by `retry` there's no reason to build a real error.
|
|
772
|
-
rxjs.throwError(() => null)
|
|
773
|
-
);
|
|
774
|
-
}),
|
|
775
|
-
rxjs.retry(),
|
|
776
|
-
// start with a closed connection.
|
|
777
|
-
rxjs.startWith(closedConnection),
|
|
778
|
-
// prevent multiple subscriptions from re-evaluating the entire pipeline.
|
|
779
|
-
rxjs.shareReplay({ bufferSize: 1, refCount: false }),
|
|
780
|
-
rxjs.takeUntil(this.closeConnection)
|
|
781
|
-
);
|
|
782
|
-
/**
|
|
783
|
-
* observable which is either an instance of the current `TrueNasSocket` used for the connection
|
|
784
|
-
* or `null` if the connection is closed or errored for any reason.
|
|
785
|
-
*/
|
|
786
|
-
this.ws$ = this.connection$.pipe(
|
|
787
|
-
rxjs.map((conn) => conn.state === "active" ? conn.ws : null)
|
|
788
|
-
);
|
|
789
|
-
/**
|
|
790
|
-
* current hostname we used to build the URL for this connection.
|
|
791
|
-
*/
|
|
792
|
-
this.hostname$ = this.connection$.pipe(
|
|
793
|
-
rxjs.map((conn) => conn.state === "active" ? conn.hostname : null)
|
|
794
|
-
);
|
|
795
|
-
/**
|
|
796
|
-
* emits error messages from the connection pipeline. this is used to derive the
|
|
797
|
-
* corresponding compatibility property.
|
|
798
|
-
*/
|
|
799
|
-
this.lastErrorMessage$ = this.connection$.pipe(
|
|
800
|
-
rxjs.map((conn) => conn.state === "error" ? conn.message : null)
|
|
801
|
-
);
|
|
802
|
-
/**
|
|
803
|
-
* emits when the connection state *changes*, so it'll never emit `true` twice
|
|
804
|
-
* in a row nor `false` twice in a row.
|
|
805
|
-
*/
|
|
806
|
-
this.opened$ = this.connection$.pipe(
|
|
807
|
-
rxjs.map((conn) => conn.state),
|
|
808
|
-
rxjs.distinctUntilChanged(),
|
|
809
|
-
rxjs.map((state) => state === "active")
|
|
810
|
-
);
|
|
811
|
-
/**
|
|
812
|
-
* emits when the connection state goes from open to closed.
|
|
813
|
-
*/
|
|
814
|
-
this.closed$ = this.connection$.pipe(
|
|
815
|
-
rxjs.map((conn) => conn.state),
|
|
816
|
-
rxjs.distinctUntilChanged(),
|
|
817
|
-
rxjs.filter((state) => state === "closed" || state === "error"),
|
|
818
|
-
rxjs.map(() => {
|
|
819
|
-
})
|
|
820
|
-
);
|
|
821
|
-
/**
|
|
822
|
-
* whether to display a connection error to the user — the **live** signal.
|
|
823
|
-
*
|
|
824
|
-
* `true` only while `connection$` is currently a `ConnectionError`, meaning the entire race +
|
|
825
|
-
* retry cycle has been exhausted for all hostnames right now; it flips back to `false` once a
|
|
826
|
-
* connection is re-established. individual socket losses during a race are expected and not surfaced.
|
|
827
|
-
*
|
|
828
|
-
* NOTE: this is NOT the same as the `hasExhaustedRetries()` method, which is a **cumulative**
|
|
829
|
-
* snapshot (see there). The two can disagree — prefer this observable for "is the connection
|
|
830
|
-
* errored right now?".
|
|
831
|
-
*/
|
|
832
|
-
this.hasConnectionError$ = this.connection$.pipe(
|
|
833
|
-
rxjs.map((conn) => conn.state === "error"),
|
|
834
|
-
rxjs.takeUntil(this.closeConnection)
|
|
835
|
-
);
|
|
836
|
-
/**
|
|
837
|
-
* observable which always emits messages from the current socket.
|
|
838
|
-
*/
|
|
839
|
-
this.messages$ = this.ws$.pipe(
|
|
840
|
-
rxjs.filter((ws) => ws !== null),
|
|
841
|
-
rxjs.switchMap((ws) => ws.messages().pipe(
|
|
842
|
-
rxjs.catchError(() => rxjs.EMPTY)
|
|
843
|
-
))
|
|
844
|
-
);
|
|
845
|
-
rxjs.combineLatest([
|
|
846
|
-
rxjs.interval(twentySeconds),
|
|
847
|
-
this.ws$
|
|
848
|
-
]).pipe(
|
|
849
|
-
rxjs.takeUntil(this.closeConnection)
|
|
850
|
-
).subscribe(([, ws]) => {
|
|
851
|
-
if (ws) {
|
|
852
|
-
const pingMessage = createJsonRpcMessage("core.ping");
|
|
853
|
-
ws.next(pingMessage);
|
|
854
|
-
}
|
|
855
|
-
});
|
|
856
|
-
this.opened$.subscribe((val) => this.opened.next(val));
|
|
857
|
-
this.closed$.subscribe(() => this.closed.next());
|
|
858
|
-
this.hostname$.pipe(rxjs.filter(Boolean)).subscribe((name) => this.hostname.next(name));
|
|
859
|
-
this.ws$.pipe(rxjs.filter(Boolean)).subscribe((ws) => this.ws = ws);
|
|
860
|
-
this.lastErrorMessage$.subscribe((msg) => this.lastErrorMessage.next(msg));
|
|
861
|
-
this.enabled$.next(initialEnabled);
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* whether the connection has exhausted its retries — the **cumulative** snapshot, read
|
|
865
|
-
* synchronously. (Formerly `hasConnectionError()`; renamed to disambiguate it from the
|
|
866
|
-
* live `hasConnectionError$` observable, with which it can disagree.)
|
|
867
|
-
*
|
|
868
|
-
* `true` when the lifetime `connectionAttempts` count exceeds `hostnames.length * maxRetry`,
|
|
869
|
-
* OR when an error message is currently set. Ported from the source (tncui) behavior.
|
|
870
|
-
*/
|
|
871
|
-
hasExhaustedRetries() {
|
|
872
|
-
const attemptsExhausted = this.connectionAttempts.value > this.hostnames.length * this.maxRetry;
|
|
873
|
-
const hasErrorMessage = this.lastErrorMessage.value !== null;
|
|
874
|
-
return attemptsExhausted || hasErrorMessage;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* enables or disables the connection gate. the app calls this when its `SystemState`
|
|
878
|
-
* changes (mapping `SystemState.Active -> true`, everything else -> `false`).
|
|
879
|
-
*/
|
|
880
|
-
setEnabled(enabled) {
|
|
881
|
-
this.enabled$.next(enabled);
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* compatibility method which just returns the public `messages$` observable.
|
|
885
|
-
* this is used in a few places in the codebase that relied upon the old implementation.
|
|
886
|
-
*/
|
|
887
|
-
messages() {
|
|
888
|
-
return this.messages$;
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* sends a message over the current websocket OR queues it to send once
|
|
892
|
-
* the next websocket is opened. messages sent via `send` will not be lost until after
|
|
893
|
-
* the next `closed$` emission.
|
|
894
|
-
*/
|
|
895
|
-
send(message) {
|
|
896
|
-
return this.ws$.pipe(
|
|
897
|
-
rxjs.filter((ws) => ws !== null),
|
|
898
|
-
rxjs.take(1)
|
|
899
|
-
).subscribe((ws) => ws.next(message));
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* manually closes this connection and prevents it from retrying/opening any more.
|
|
903
|
-
*/
|
|
904
|
-
close() {
|
|
905
|
-
this.closeConnection.next();
|
|
906
|
-
this.closeConnection.complete();
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* helper method which creates an `Observable` which emits an `ActiveConnection`
|
|
910
|
-
* after it establishes a connection to the given hostname. the observable will emit an
|
|
911
|
-
* error if the connection is never established and will not complete until unsubscribed from or closed.
|
|
912
|
-
*/
|
|
913
|
-
createSocket(hostname) {
|
|
914
|
-
const url = `${socketScheme(this.protocol)}//${hostname}${this.websocketPath}`;
|
|
915
|
-
let hasOpened = false;
|
|
916
|
-
return new rxjs.Observable((subscriber) => {
|
|
917
|
-
const ws = new TrueNasSocket({
|
|
918
|
-
url,
|
|
919
|
-
openObserver: {
|
|
920
|
-
next: () => {
|
|
921
|
-
hasOpened = true;
|
|
922
|
-
this.connectionAttempts.next(0);
|
|
923
|
-
subscriber.next({ ws, hostname });
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
closeObserver: {
|
|
927
|
-
next: (event) => {
|
|
928
|
-
const reason = event.reason || "";
|
|
929
|
-
let errorMessage;
|
|
930
|
-
if (isHttpStatusError(reason)) {
|
|
931
|
-
errorMessage = getHttpError(reason);
|
|
932
|
-
} else {
|
|
933
|
-
errorMessage = getWebSocketError(event.code);
|
|
934
|
-
}
|
|
935
|
-
this.connectionAttempts.next(this.connectionAttempts.value + 1);
|
|
936
|
-
subscriber.error(makeConnectionError(errorMessage, hostname));
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
const startupSub = ws.socket.subscribe({
|
|
941
|
-
error: () => {
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
return () => {
|
|
945
|
-
startupSub.unsubscribe();
|
|
946
|
-
ws.complete();
|
|
947
|
-
};
|
|
948
|
-
}).pipe(
|
|
949
|
-
// retry logic:
|
|
950
|
-
// * if a connection is not established in 10 seconds, consider that an error
|
|
951
|
-
rxjs.timeout({ first: tenSeconds }),
|
|
952
|
-
rxjs.retry({
|
|
953
|
-
// * while still *establishing*: wait `retryDelay` before trying again, giving up
|
|
954
|
-
// after `maxRetry` retries.
|
|
955
|
-
count: this.maxRetry,
|
|
956
|
-
// * we use a custom `delay` function here to ensure no retries are performed
|
|
957
|
-
// once a socket is *opened*.
|
|
958
|
-
// basically: a later death must propagate out rather than being retried
|
|
959
|
-
// so it can be handled by the `connection$` observable.
|
|
960
|
-
delay: (error) => {
|
|
961
|
-
if (hasOpened) {
|
|
962
|
-
return rxjs.throwError(() => error);
|
|
963
|
-
}
|
|
964
|
-
return rxjs.timer(this.retryDelay);
|
|
965
|
-
}
|
|
966
|
-
})
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* helper function which actually performs the parallel connection `race`.
|
|
971
|
-
*/
|
|
972
|
-
connect() {
|
|
973
|
-
return rxjs.race(
|
|
974
|
-
this.hostnames.map(this.createSocket.bind(this))
|
|
975
|
-
).pipe(
|
|
976
|
-
rxjs.tap((conn) => this.logger.debug(`TrueNas socket opened to ${conn.hostname}.`)),
|
|
977
|
-
rxjs.takeUntil(this.closeConnection)
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
};
|
|
981
|
-
var makeActiveConnection = (ws, hostname) => ({
|
|
982
|
-
ws,
|
|
983
|
-
hostname,
|
|
984
|
-
state: "active"
|
|
985
|
-
});
|
|
986
|
-
var makeConnectionError = (message, hostname) => ({
|
|
987
|
-
name: "ConnectionError",
|
|
988
|
-
message,
|
|
989
|
-
hostname,
|
|
990
|
-
state: "error"
|
|
991
|
-
});
|
|
992
|
-
var closedConnection = { state: "closed" };
|
|
993
|
-
|
|
994
|
-
// src/generated/v25_10_0/index.ts
|
|
995
|
-
var v25_10_0_exports = {};
|
|
996
|
-
__export(v25_10_0_exports, {
|
|
997
|
-
ACLTemplateEntryAcltypeInput: () => ACLTemplateEntryAcltypeInput,
|
|
998
|
-
Aclmode: () => Aclmode,
|
|
999
|
-
Acltype: () => Acltype,
|
|
1000
|
-
Action: () => Action,
|
|
1001
|
-
Advpowermgmt: () => Advpowermgmt,
|
|
1002
|
-
Algorithm: () => Algorithm,
|
|
1003
|
-
AppContainerDetailsStateInput: () => AppContainerDetailsStateInput,
|
|
1004
|
-
Atime: () => Atime,
|
|
1005
|
-
Authenticator: () => Authenticator,
|
|
1006
|
-
Authmethod: () => Authmethod,
|
|
1007
|
-
Autotrim: () => Autotrim,
|
|
1008
|
-
Basic: () => Basic,
|
|
1009
|
-
Blocksize: () => Blocksize,
|
|
1010
|
-
Bootloader: () => Bootloader,
|
|
1011
|
-
Casesensitivity: () => Casesensitivity,
|
|
1012
|
-
CloudBackupSnapshotItemType: () => CloudBackupSnapshotItemType,
|
|
1013
|
-
CloudSyncCreateDirection: () => CloudSyncCreateDirection,
|
|
1014
|
-
CloudSyncCreateDirectionInput: () => CloudSyncCreateDirectionInput,
|
|
1015
|
-
CloudTaskAttributesEncryptionInput: () => CloudTaskAttributesEncryptionInput,
|
|
1016
|
-
Code: () => Code,
|
|
1017
|
-
Compression: () => Compression,
|
|
1018
|
-
CorePingRemoteTypeInput: () => CorePingRemoteTypeInput,
|
|
1019
|
-
Credentials: () => Credentials,
|
|
1020
|
-
DeviceGetInfoOtherTypeInput: () => DeviceGetInfoOtherTypeInput,
|
|
1021
|
-
DiskDetailsTypeInput: () => DiskDetailsTypeInput,
|
|
1022
|
-
DiskWipeModeInput: () => DiskWipeModeInput,
|
|
1023
|
-
DockerStatusInfoStatusInput: () => DockerStatusInfoStatusInput,
|
|
1024
|
-
Enclosure2SetSlotStatusStatusInput: () => Enclosure2SetSlotStatusStatusInput,
|
|
1025
|
-
Encryption: () => Encryption,
|
|
1026
|
-
Endpoint: () => Endpoint,
|
|
1027
|
-
Exec: () => Exec,
|
|
1028
|
-
Feature: () => Feature,
|
|
1029
|
-
FilesystemDirEntryType: () => FilesystemDirEntryType,
|
|
1030
|
-
Function: () => Function,
|
|
1031
|
-
GetDisplayDeviceType: () => GetDisplayDeviceType,
|
|
1032
|
-
Hddstandby: () => Hddstandby,
|
|
1033
|
-
InitShutdownScriptCreateType: () => InitShutdownScriptCreateType,
|
|
1034
|
-
InitShutdownScriptCreateTypeInput: () => InitShutdownScriptCreateTypeInput,
|
|
1035
|
-
InterfaceCreateAliasTypeInput: () => InterfaceCreateAliasTypeInput,
|
|
1036
|
-
InterfaceCreateTypeInput: () => InterfaceCreateTypeInput,
|
|
1037
|
-
Iotype: () => Iotype,
|
|
1038
|
-
IpmiChassisIdentifyVerb: () => IpmiChassisIdentifyVerb,
|
|
1039
|
-
IscsiExtentCreateType: () => IscsiExtentCreateType,
|
|
1040
|
-
IscsiExtentCreateTypeInput: () => IscsiExtentCreateTypeInput,
|
|
1041
|
-
IscsiTargetCreateMode: () => IscsiTargetCreateMode,
|
|
1042
|
-
IscsiTargetCreateModeInput: () => IscsiTargetCreateModeInput,
|
|
1043
|
-
KeychainCredentialEntryType: () => KeychainCredentialEntryType,
|
|
1044
|
-
KeychainCredentialEntryTypeInput: () => KeychainCredentialEntryTypeInput,
|
|
1045
|
-
Level: () => Level,
|
|
1046
|
-
NFS4ACEType: () => NFS4ACEType,
|
|
1047
|
-
NFS4ACETypeInput: () => NFS4ACETypeInput,
|
|
1048
|
-
Name: () => Name,
|
|
1049
|
-
Policy: () => Policy,
|
|
1050
|
-
PoolCreateChecksum: () => PoolCreateChecksum,
|
|
1051
|
-
PoolCreateDeduplication: () => PoolCreateDeduplication,
|
|
1052
|
-
PoolCreateTopologyDataVdevDRAIDTypeInput: () => PoolCreateTopologyDataVdevDRAIDTypeInput,
|
|
1053
|
-
PoolDatasetCreateFilesystemAcltypeInput: () => PoolDatasetCreateFilesystemAcltypeInput,
|
|
1054
|
-
PoolDatasetCreateFilesystemChecksum: () => PoolDatasetCreateFilesystemChecksum,
|
|
1055
|
-
PoolDatasetCreateFilesystemDeduplication: () => PoolDatasetCreateFilesystemDeduplication,
|
|
1056
|
-
PoolDatasetCreateFilesystemReadonlyInput: () => PoolDatasetCreateFilesystemReadonlyInput,
|
|
1057
|
-
PoolDatasetDeleteResult: () => PoolDatasetDeleteResult,
|
|
1058
|
-
PoolScanStateInput: () => PoolScanStateInput,
|
|
1059
|
-
Protocol: () => Protocol,
|
|
1060
|
-
Purpose: () => Purpose,
|
|
1061
|
-
Readonly: () => Readonly,
|
|
1062
|
-
ReplicationCountEligibleManualSnapshotsTransportInput: () => ReplicationCountEligibleManualSnapshotsTransportInput,
|
|
1063
|
-
ReplicationCreateReadonlyInput: () => ReplicationCreateReadonlyInput,
|
|
1064
|
-
Resolution: () => Resolution,
|
|
1065
|
-
Rpm: () => Rpm,
|
|
1066
|
-
RsyncTaskCreateDirection: () => RsyncTaskCreateDirection,
|
|
1067
|
-
RsyncTaskCreateDirectionInput: () => RsyncTaskCreateDirectionInput,
|
|
1068
|
-
RsyncTaskCreateMode: () => RsyncTaskCreateMode,
|
|
1069
|
-
RsyncTaskCreateModeInput: () => RsyncTaskCreateModeInput,
|
|
1070
|
-
SMBEntryEncryptionInput: () => SMBEntryEncryptionInput,
|
|
1071
|
-
STIGType: () => STIGType,
|
|
1072
|
-
Schema: () => Schema,
|
|
1073
|
-
Security: () => Security,
|
|
1074
|
-
Serialspeed: () => Serialspeed,
|
|
1075
|
-
ServiceControlVerb: () => ServiceControlVerb,
|
|
1076
|
-
SharingSMBEntryPurposeInput: () => SharingSMBEntryPurposeInput,
|
|
1077
|
-
SharingSMBEntryPurposeInput2: () => SharingSMBEntryPurposeInput2,
|
|
1078
|
-
Shutdown: () => Shutdown,
|
|
1079
|
-
Snapdev: () => Snapdev,
|
|
1080
|
-
Snapdir: () => Snapdir,
|
|
1081
|
-
Source: () => Source,
|
|
1082
|
-
SourceInput: () => SourceInput,
|
|
1083
|
-
State: () => State,
|
|
1084
|
-
Status: () => Status,
|
|
1085
|
-
SupportNewTicketCommunityTypeInput: () => SupportNewTicketCommunityTypeInput,
|
|
1086
|
-
Sync: () => Sync,
|
|
1087
|
-
SyslogServerTransportInput: () => SyslogServerTransportInput,
|
|
1088
|
-
Sysloglevel: () => Sysloglevel,
|
|
1089
|
-
SystemProductTypeResult: () => SystemProductTypeResult,
|
|
1090
|
-
SystemStateResult: () => SystemStateResult,
|
|
1091
|
-
Tag: () => Tag,
|
|
1092
|
-
Target: () => Target,
|
|
1093
|
-
Time: () => Time,
|
|
1094
|
-
Transport: () => Transport,
|
|
1095
|
-
TruecommandConfigChangedEventFieldsStatus: () => TruecommandConfigChangedEventFieldsStatus,
|
|
1096
|
-
TruecommandConfigChangedEventFieldsStatusInput: () => TruecommandConfigChangedEventFieldsStatusInput,
|
|
1097
|
-
TunableCreateType: () => TunableCreateType,
|
|
1098
|
-
TunableCreateTypeInput: () => TunableCreateTypeInput,
|
|
1099
|
-
UPSEntryMode: () => UPSEntryMode,
|
|
1100
|
-
UPSEntryModeInput: () => UPSEntryModeInput,
|
|
1101
|
-
Unixcharset: () => Unixcharset,
|
|
1102
|
-
Username: () => Username,
|
|
1103
|
-
VMDiskDeviceType: () => VMDiskDeviceType,
|
|
1104
|
-
VMDiskDeviceTypeInput: () => VMDiskDeviceTypeInput,
|
|
1105
|
-
VMWareEntryStateStateInput: () => VMWareEntryStateStateInput,
|
|
1106
|
-
VMWareMatchDatastoresWithDatasetsResultFilesystemType: () => VMWareMatchDatastoresWithDatasetsResultFilesystemType,
|
|
1107
|
-
Vendor: () => Vendor,
|
|
1108
|
-
Volblocksize: () => Volblocksize,
|
|
1109
|
-
When: () => When,
|
|
1110
|
-
ZfsTierRewriteJobEntryStatusInput: () => ZfsTierRewriteJobEntryStatusInput
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
// src/generated/v25_10_0/api-types.ts
|
|
1114
|
-
var Aclmode = {
|
|
1115
|
-
Passthrough: "PASSTHROUGH",
|
|
1116
|
-
Restricted: "RESTRICTED",
|
|
1117
|
-
Discard: "DISCARD",
|
|
1118
|
-
Inherit: "INHERIT"
|
|
1119
|
-
};
|
|
1120
|
-
var ACLTemplateEntryAcltypeInput = {
|
|
1121
|
-
Nfs4: "NFS4",
|
|
1122
|
-
Posix1e: "POSIX1E"
|
|
1123
|
-
};
|
|
1124
|
-
var Acltype = {
|
|
1125
|
-
Nfs4: "NFS4",
|
|
1126
|
-
Posix1e: "POSIX1E"
|
|
1127
|
-
};
|
|
1128
|
-
var Action = {
|
|
1129
|
-
Start: "START",
|
|
1130
|
-
Stop: "STOP",
|
|
1131
|
-
Pause: "PAUSE"
|
|
1132
|
-
};
|
|
1133
|
-
var Advpowermgmt = {
|
|
1134
|
-
Disabled: "DISABLED",
|
|
1135
|
-
"1": "1",
|
|
1136
|
-
"64": "64",
|
|
1137
|
-
"127": "127",
|
|
1138
|
-
"128": "128",
|
|
1139
|
-
"192": "192",
|
|
1140
|
-
"254": "254"
|
|
511
|
+
var Advpowermgmt = {
|
|
512
|
+
Disabled: "DISABLED",
|
|
513
|
+
"1": "1",
|
|
514
|
+
"64": "64",
|
|
515
|
+
"127": "127",
|
|
516
|
+
"128": "128",
|
|
517
|
+
"192": "192",
|
|
518
|
+
"254": "254"
|
|
1141
519
|
};
|
|
1142
520
|
var Algorithm = {
|
|
1143
521
|
Aes128Ccm: "AES-128-CCM",
|
|
@@ -2872,141 +2250,871 @@ __export(v27_0_0_exports, {
|
|
|
2872
2250
|
ZpoolScrubRunAction: () => ZpoolScrubRunAction
|
|
2873
2251
|
});
|
|
2874
2252
|
|
|
2875
|
-
// src/generated/index.ts
|
|
2876
|
-
var SUPPORTED_API_VERSIONS = [
|
|
2877
|
-
"v25.10.0",
|
|
2878
|
-
"v25.10.1",
|
|
2879
|
-
"v25.10.2",
|
|
2880
|
-
"v25.10.3",
|
|
2881
|
-
"v25.10.4",
|
|
2882
|
-
"v25.10.5",
|
|
2883
|
-
"v26.0.0",
|
|
2884
|
-
"v27.0.0"
|
|
2885
|
-
];
|
|
2253
|
+
// src/generated/index.ts
|
|
2254
|
+
var SUPPORTED_API_VERSIONS = [
|
|
2255
|
+
"v25.10.0",
|
|
2256
|
+
"v25.10.1",
|
|
2257
|
+
"v25.10.2",
|
|
2258
|
+
"v25.10.3",
|
|
2259
|
+
"v25.10.4",
|
|
2260
|
+
"v25.10.5",
|
|
2261
|
+
"v26.0.0",
|
|
2262
|
+
"v27.0.0"
|
|
2263
|
+
];
|
|
2264
|
+
|
|
2265
|
+
// src/config/api-version.config.ts
|
|
2266
|
+
var apiVersionConfig = {
|
|
2267
|
+
/**
|
|
2268
|
+
* Minimum supported API version. Systems below it are rejected.
|
|
2269
|
+
*
|
|
2270
|
+
* DERIVED, not declared. The client supports exactly the versions it ships
|
|
2271
|
+
* types for, so the oldest generated version *is* the minimum — there is no
|
|
2272
|
+
* second decision to make, and therefore nothing to keep in sync.
|
|
2273
|
+
*
|
|
2274
|
+
* To move the floor, change `--min-version` in the `generate:api` script in
|
|
2275
|
+
* package.json and regenerate; this follows on its own. Written as a literal
|
|
2276
|
+
* it would duplicate that value with nothing enforcing agreement, so raising
|
|
2277
|
+
* one and forgetting the other would silently keep generating types for
|
|
2278
|
+
* versions the client rejects.
|
|
2279
|
+
*/
|
|
2280
|
+
MIN_SUPPORTED_VERSION: SUPPORTED_API_VERSIONS[0],
|
|
2281
|
+
/**
|
|
2282
|
+
* Maximum supported API version. Systems above it are rejected.
|
|
2283
|
+
*
|
|
2284
|
+
* NOT derived, deliberately — and for a different reason than it used to
|
|
2285
|
+
* lag. It sat at v26.0.0 while types for v27 already shipped, because
|
|
2286
|
+
* `CLIENT_BY_VERSION_KEY` had no `27` and a v27 system would have passed the
|
|
2287
|
+
* range check only to throw on client selection. `TrueNasApiClientV27` now
|
|
2288
|
+
* exists, so this moves up with it.
|
|
2289
|
+
*
|
|
2290
|
+
* What it must not become is `SUPPORTED_API_VERSIONS[length - 1]`, the mirror
|
|
2291
|
+
* of how MIN is derived. MIN is safe to derive because the oldest generated
|
|
2292
|
+
* version always has a client — it is the floor the clients were built from.
|
|
2293
|
+
* The ceiling is not symmetric: generating types for v28 does not write a v28
|
|
2294
|
+
* client, so deriving this would re-create the exact gap described above on
|
|
2295
|
+
* the next regeneration, silently. The real invariant is "the newest version
|
|
2296
|
+
* a client can be built for", which lives in `CLIENT_BY_VERSION_KEY`; a
|
|
2297
|
+
* factory test asserts the two agree, which is the check that keeps this
|
|
2298
|
+
* literal honest without importing the factory here.
|
|
2299
|
+
*/
|
|
2300
|
+
MAX_SUPPORTED_VERSION: "v27.0.0",
|
|
2301
|
+
/**
|
|
2302
|
+
* Fallback version to use when version discovery fails due to CORS/network errors.
|
|
2303
|
+
* When /api/versions returns HTTP status 0 (CORS block, network down, etc.),
|
|
2304
|
+
* the system will attempt to connect using this version as a best-effort fallback.
|
|
2305
|
+
*
|
|
2306
|
+
* WARNING: Status 0 errors have multiple causes:
|
|
2307
|
+
* - CORS policy blocking the request
|
|
2308
|
+
* - Network disconnected
|
|
2309
|
+
* - DNS lookup failure
|
|
2310
|
+
* - Server unreachable
|
|
2311
|
+
*
|
|
2312
|
+
* Using this fallback means systems with genuine network issues will still
|
|
2313
|
+
* attempt connection (and fail during WebSocket handshake instead of immediately).
|
|
2314
|
+
*/
|
|
2315
|
+
FALLBACK_VERSION: "v25.10.0"
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
// src/types/api-version.type.ts
|
|
2319
|
+
var VersionCompatibility = /* @__PURE__ */ ((VersionCompatibility2) => {
|
|
2320
|
+
VersionCompatibility2["Compatible"] = "compatible";
|
|
2321
|
+
VersionCompatibility2["TooOld"] = "too-old";
|
|
2322
|
+
VersionCompatibility2["TooNew"] = "too-new";
|
|
2323
|
+
VersionCompatibility2["Invalid"] = "invalid";
|
|
2324
|
+
return VersionCompatibility2;
|
|
2325
|
+
})(VersionCompatibility || {});
|
|
2326
|
+
|
|
2327
|
+
// src/utils/api-version.utils.ts
|
|
2328
|
+
var legacyCutoffYear = 25;
|
|
2329
|
+
function parseApiVersion(versionString) {
|
|
2330
|
+
const regex = /^v(\d{2})\.(\d+)\.(\d+)$/;
|
|
2331
|
+
const match = versionString.match(regex);
|
|
2332
|
+
if (!match) {
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
const [, yearStr, minorStr, patchStr] = match;
|
|
2336
|
+
const year = parseInt(yearStr, 10);
|
|
2337
|
+
const minor = parseInt(minorStr, 10);
|
|
2338
|
+
const patch = parseInt(patchStr, 10);
|
|
2339
|
+
if (year <= legacyCutoffYear) {
|
|
2340
|
+
if (minor < 1 || minor > 12) {
|
|
2341
|
+
return null;
|
|
2342
|
+
}
|
|
2343
|
+
} else if (year >= 26) {
|
|
2344
|
+
if (minor < 0 || minor > 99) {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return {
|
|
2349
|
+
version: versionString,
|
|
2350
|
+
year,
|
|
2351
|
+
minor,
|
|
2352
|
+
patch,
|
|
2353
|
+
websocketPath: `/api/${versionString}`
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function compareVersions(a, b) {
|
|
2357
|
+
if (a.year !== b.year) {
|
|
2358
|
+
return a.year - b.year;
|
|
2359
|
+
}
|
|
2360
|
+
if (a.minor !== b.minor) {
|
|
2361
|
+
return a.minor - b.minor;
|
|
2362
|
+
}
|
|
2363
|
+
return a.patch - b.patch;
|
|
2364
|
+
}
|
|
2365
|
+
function checkVersionCompatibility(version) {
|
|
2366
|
+
const minVersion = parseApiVersion(apiVersionConfig.MIN_SUPPORTED_VERSION);
|
|
2367
|
+
const maxVersion = parseApiVersion(apiVersionConfig.MAX_SUPPORTED_VERSION);
|
|
2368
|
+
if (!minVersion || !maxVersion) {
|
|
2369
|
+
return "invalid" /* Invalid */;
|
|
2370
|
+
}
|
|
2371
|
+
if (compareVersions(version, minVersion) < 0) {
|
|
2372
|
+
return "too-old" /* TooOld */;
|
|
2373
|
+
}
|
|
2374
|
+
if (compareVersions(version, maxVersion) > 0) {
|
|
2375
|
+
return "too-new" /* TooNew */;
|
|
2376
|
+
}
|
|
2377
|
+
return "compatible" /* Compatible */;
|
|
2378
|
+
}
|
|
2379
|
+
function isVersionSupported(version) {
|
|
2380
|
+
return checkVersionCompatibility(version) === "compatible" /* Compatible */;
|
|
2381
|
+
}
|
|
2382
|
+
function filterCompatibleVersions(versions) {
|
|
2383
|
+
return versions.filter(isVersionSupported);
|
|
2384
|
+
}
|
|
2385
|
+
function getWebSocketPath(version) {
|
|
2386
|
+
return version.websocketPath;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/enums/truenas-auth-mechanism.enum.ts
|
|
2390
|
+
var TrueNasAuthMechanism = /* @__PURE__ */ ((TrueNasAuthMechanism2) => {
|
|
2391
|
+
TrueNasAuthMechanism2["ApiKey"] = "API_KEY_PLAIN";
|
|
2392
|
+
TrueNasAuthMechanism2["Password"] = "PASSWORD_PLAIN";
|
|
2393
|
+
TrueNasAuthMechanism2["Otp"] = "OTP_TOKEN";
|
|
2394
|
+
TrueNasAuthMechanism2["Token"] = "TOKEN_PLAIN";
|
|
2395
|
+
return TrueNasAuthMechanism2;
|
|
2396
|
+
})(TrueNasAuthMechanism || {});
|
|
2397
|
+
|
|
2398
|
+
// src/errors/auth.errors.ts
|
|
2399
|
+
var AuthErrorCode = /* @__PURE__ */ ((AuthErrorCode2) => {
|
|
2400
|
+
AuthErrorCode2["PasswordAuthFailed"] = "PASSWORD_AUTH_FAILED";
|
|
2401
|
+
AuthErrorCode2["OtpAuthFailed"] = "OTP_AUTH_FAILED";
|
|
2402
|
+
AuthErrorCode2["ApiKeyAuthFailed"] = "API_KEY_AUTH_FAILED";
|
|
2403
|
+
AuthErrorCode2["TokenAuthFailed"] = "TOKEN_AUTH_FAILED";
|
|
2404
|
+
AuthErrorCode2["FullAdminRequired"] = "FULL_ADMIN_REQUIRED";
|
|
2405
|
+
return AuthErrorCode2;
|
|
2406
|
+
})(AuthErrorCode || {});
|
|
2407
|
+
var AuthError = class _AuthError extends Error {
|
|
2408
|
+
constructor(code, message) {
|
|
2409
|
+
super(message);
|
|
2410
|
+
this.code = code;
|
|
2411
|
+
this.name = "AuthError";
|
|
2412
|
+
Object.setPrototypeOf(this, _AuthError.prototype);
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
// src/auth/truenas-authenticator.ts
|
|
2417
|
+
var throwOnAuthenticationFailure = (code, message) => rxjs.switchMap((response) => {
|
|
2418
|
+
if (response.response_type === "AUTH_ERR" /* AuthErr */) {
|
|
2419
|
+
return rxjs.throwError(() => new AuthError(code, message));
|
|
2420
|
+
}
|
|
2421
|
+
return rxjs.of(response);
|
|
2422
|
+
});
|
|
2423
|
+
var TrueNasAuthenticator = class _TrueNasAuthenticator {
|
|
2424
|
+
constructor(connection, version) {
|
|
2425
|
+
this.connection = connection;
|
|
2426
|
+
this.version = version;
|
|
2427
|
+
// in seconds (5 minutes)
|
|
2428
|
+
/**
|
|
2429
|
+
* whether or not the system is currently authenticated and accessible.
|
|
2430
|
+
* this is a `BehaviorSubject` instead of a signal, since we use its `getValue` method
|
|
2431
|
+
* throughout a lot of the codebase.
|
|
2432
|
+
*/
|
|
2433
|
+
this.authenticated$ = new rxjs.BehaviorSubject(false);
|
|
2434
|
+
/**
|
|
2435
|
+
* `true` when the system is currently being authenticated to, and `false` otherwise.
|
|
2436
|
+
* does not indicate whatsoever if authentication is successful or not, just whether the process
|
|
2437
|
+
* is ongoing.
|
|
2438
|
+
*/
|
|
2439
|
+
this.authenticating$ = new rxjs.BehaviorSubject(false);
|
|
2440
|
+
this.credentials = { username: "", password: "", key: "" };
|
|
2441
|
+
this.sessionLifetime = _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2442
|
+
this.connection.opened.pipe(
|
|
2443
|
+
rxjs.filter(
|
|
2444
|
+
(isOpen) => !!(isOpen && this.credentials?.username && (this.credentials.password || this.credentials.key))
|
|
2445
|
+
),
|
|
2446
|
+
rxjs.switchMap(() => {
|
|
2447
|
+
if (this.credentials.password) {
|
|
2448
|
+
return this.loginWithUserPass(
|
|
2449
|
+
this.credentials.username,
|
|
2450
|
+
this.credentials.password
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
return this.loginWithApiKey({
|
|
2454
|
+
username: this.credentials.username,
|
|
2455
|
+
key: this.credentials.key
|
|
2456
|
+
});
|
|
2457
|
+
})
|
|
2458
|
+
).subscribe();
|
|
2459
|
+
connection.closed.subscribe(() => {
|
|
2460
|
+
this.sessionLifetime = _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2461
|
+
this.authenticated$.next(false);
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
static {
|
|
2465
|
+
this.DefaultSessionLifetime = 300;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* `login_options` asking for a reconnect token, when the server understands it.
|
|
2469
|
+
*
|
|
2470
|
+
* Omitted below v26. `AuthCommonOptions` is `additionalProperties: false`
|
|
2471
|
+
* there and has only `user_info`, so sending the member is a validation
|
|
2472
|
+
* error, not an ignored field — it would fail login outright on the oldest
|
|
2473
|
+
* version this client supports.
|
|
2474
|
+
*/
|
|
2475
|
+
/**
|
|
2476
|
+
* Two things this does not do, both deliberate and both worth knowing.
|
|
2477
|
+
*
|
|
2478
|
+
* There is no way for a consumer to decline: every v26+ password login now
|
|
2479
|
+
* mints a single-use credential carrying that session's roles, whether or not
|
|
2480
|
+
* the caller wants one. And the auto-relogin in the constructor subscribes
|
|
2481
|
+
* with no observer, so the token it mints is dropped — a caller reconnecting
|
|
2482
|
+
* repeatedly holds an ageing token while the appliance mints fresh ones
|
|
2483
|
+
* nobody reads. Tokens are single-use with a 600s TTL, so that is the
|
|
2484
|
+
* reconnect case the feature is named for.
|
|
2485
|
+
*/
|
|
2486
|
+
reconnectTokenOption() {
|
|
2487
|
+
if (!this.version || this.version.year <= legacyCutoffYear) return void 0;
|
|
2488
|
+
return { login_options: { reconnect_token: true } };
|
|
2489
|
+
}
|
|
2490
|
+
loginWithUserPass(username, password) {
|
|
2491
|
+
const message = createJsonRpcMessage("auth.login_ex", [
|
|
2492
|
+
{
|
|
2493
|
+
mechanism: "PASSWORD_PLAIN" /* Password */,
|
|
2494
|
+
username,
|
|
2495
|
+
password,
|
|
2496
|
+
...this.reconnectTokenOption()
|
|
2497
|
+
}
|
|
2498
|
+
]);
|
|
2499
|
+
this.authenticating$.next(true);
|
|
2500
|
+
this.connection.send(message);
|
|
2501
|
+
const messageId = message.id ?? "";
|
|
2502
|
+
return this.connection.messages().pipe(
|
|
2503
|
+
withId(messageId),
|
|
2504
|
+
rxjs.map((msg) => {
|
|
2505
|
+
if (msg.error) {
|
|
2506
|
+
const errorMessage = getApiErrorMessage(
|
|
2507
|
+
msg.error,
|
|
2508
|
+
"Authentication failed"
|
|
2509
|
+
);
|
|
2510
|
+
throw new Error(errorMessage);
|
|
2511
|
+
}
|
|
2512
|
+
return msg.result;
|
|
2513
|
+
}),
|
|
2514
|
+
throwOnAuthenticationFailure(
|
|
2515
|
+
"PASSWORD_AUTH_FAILED" /* PasswordAuthFailed */,
|
|
2516
|
+
"TrueNAS authentication failed. Please verify your TrueNAS user credentials and try again."
|
|
2517
|
+
),
|
|
2518
|
+
rxjs.tap((res) => {
|
|
2519
|
+
if (res.response_type === "SUCCESS" /* Success */) {
|
|
2520
|
+
if (res.user_info?.privilege.roles.$set.includes("FULL_ADMIN" /* FullAdmin */)) {
|
|
2521
|
+
this.credentials.username = username;
|
|
2522
|
+
this.credentials.password = password;
|
|
2523
|
+
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2524
|
+
this.authenticated$.next(true);
|
|
2525
|
+
} else {
|
|
2526
|
+
this.logout();
|
|
2527
|
+
this.authenticated$.next(false);
|
|
2528
|
+
throw new AuthError(
|
|
2529
|
+
"FULL_ADMIN_REQUIRED" /* FullAdminRequired */,
|
|
2530
|
+
"User account must have full admin privileges"
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}),
|
|
2535
|
+
rxjs.finalize(() => {
|
|
2536
|
+
this.authenticating$.next(false);
|
|
2537
|
+
}),
|
|
2538
|
+
rxjs.take(1)
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
loginWithOtp(code) {
|
|
2542
|
+
const message = createJsonRpcMessage("auth.login_ex", [
|
|
2543
|
+
{
|
|
2544
|
+
mechanism: "OTP_TOKEN" /* Otp */,
|
|
2545
|
+
otp_token: code
|
|
2546
|
+
}
|
|
2547
|
+
]);
|
|
2548
|
+
this.authenticating$.next(true);
|
|
2549
|
+
this.connection.send(message);
|
|
2550
|
+
const messageId = message.id ?? "";
|
|
2551
|
+
return this.connection.messages().pipe(
|
|
2552
|
+
withId(messageId),
|
|
2553
|
+
rxjs.map((msg) => {
|
|
2554
|
+
if (msg.error) {
|
|
2555
|
+
const errorMessage = getApiErrorMessage(
|
|
2556
|
+
msg.error,
|
|
2557
|
+
"Authentication failed"
|
|
2558
|
+
);
|
|
2559
|
+
throw new Error(errorMessage);
|
|
2560
|
+
}
|
|
2561
|
+
return msg.result;
|
|
2562
|
+
}),
|
|
2563
|
+
rxjs.tap((res) => {
|
|
2564
|
+
if (res?.response_type === "SUCCESS" /* Success */) {
|
|
2565
|
+
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2566
|
+
this.authenticated$.next(true);
|
|
2567
|
+
}
|
|
2568
|
+
}),
|
|
2569
|
+
throwOnAuthenticationFailure(
|
|
2570
|
+
"OTP_AUTH_FAILED" /* OtpAuthFailed */,
|
|
2571
|
+
"TrueNAS authentication failed. Please verify your one-time passcode and try again."
|
|
2572
|
+
),
|
|
2573
|
+
rxjs.finalize(() => {
|
|
2574
|
+
this.authenticating$.next(false);
|
|
2575
|
+
}),
|
|
2576
|
+
rxjs.take(1)
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Re-authenticate with a token from a previous login's `reconnect_token`.
|
|
2581
|
+
*
|
|
2582
|
+
* This is what lets a second connection to the same appliance authenticate
|
|
2583
|
+
* without asking the user for a password again — middleware sessions are
|
|
2584
|
+
* per-connection, so a second socket has its own to establish.
|
|
2585
|
+
*
|
|
2586
|
+
* The token is single-use and short-lived. On v26+ a successful login mints
|
|
2587
|
+
* another on the response, so a caller keeping a session alive across
|
|
2588
|
+
* reconnects stores the newest each time; below v26 nothing is minted and
|
|
2589
|
+
* there is no chain to keep.
|
|
2590
|
+
*
|
|
2591
|
+
* Re-login is the caller's to drive. A token session is not covered by the
|
|
2592
|
+
* automatic reconnect this class does for password and api-key sessions,
|
|
2593
|
+
* which is deliberate — the token is single-use — but it means a dropped
|
|
2594
|
+
* socket needs the stored token spending explicitly. Middleware holds tokens
|
|
2595
|
+
* in memory, so a `middlewared` restart voids them, and that is a common
|
|
2596
|
+
* reason the socket dropped in the first place.
|
|
2597
|
+
*/
|
|
2598
|
+
loginWithToken(token) {
|
|
2599
|
+
const message = createJsonRpcMessage("auth.login_ex", [
|
|
2600
|
+
{
|
|
2601
|
+
mechanism: "TOKEN_PLAIN" /* Token */,
|
|
2602
|
+
token,
|
|
2603
|
+
...this.reconnectTokenOption()
|
|
2604
|
+
}
|
|
2605
|
+
]);
|
|
2606
|
+
this.authenticating$.next(true);
|
|
2607
|
+
this.connection.send(message);
|
|
2608
|
+
const messageId = message.id ?? "";
|
|
2609
|
+
return this.connection.messages().pipe(
|
|
2610
|
+
withId(messageId),
|
|
2611
|
+
rxjs.map((msg) => {
|
|
2612
|
+
if (msg.error) {
|
|
2613
|
+
const errorMessage = getApiErrorMessage(
|
|
2614
|
+
msg.error,
|
|
2615
|
+
"Authentication failed"
|
|
2616
|
+
);
|
|
2617
|
+
throw new Error(errorMessage);
|
|
2618
|
+
}
|
|
2619
|
+
return msg.result;
|
|
2620
|
+
}),
|
|
2621
|
+
// Password login lets non-`AUTH_ERR` answers through because it has to
|
|
2622
|
+
// hand `OTP_REQUIRED` back to the caller. Token login has no such case:
|
|
2623
|
+
// middleware answers `EXPIRED` for an account locked or expired between
|
|
2624
|
+
// minting and spending, and `DENIED` for a permission refusal, so letting
|
|
2625
|
+
// those reach `next` would report a failed login as a successful one.
|
|
2626
|
+
rxjs.map((res) => {
|
|
2627
|
+
if (res.response_type !== "SUCCESS" /* Success */) {
|
|
2628
|
+
throw new AuthError(
|
|
2629
|
+
"TOKEN_AUTH_FAILED" /* TokenAuthFailed */,
|
|
2630
|
+
`TrueNAS token authentication failed (${res.response_type}). The token may have expired or been used, or the account may no longer be permitted to log in.`
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
return res;
|
|
2634
|
+
}),
|
|
2635
|
+
rxjs.tap((res) => {
|
|
2636
|
+
if (res.response_type === "SUCCESS" /* Success */) {
|
|
2637
|
+
if (res.user_info?.privilege.roles.$set.includes("FULL_ADMIN" /* FullAdmin */)) {
|
|
2638
|
+
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2639
|
+
this.authenticated$.next(true);
|
|
2640
|
+
} else {
|
|
2641
|
+
this.logout();
|
|
2642
|
+
this.authenticated$.next(false);
|
|
2643
|
+
throw new AuthError(
|
|
2644
|
+
"FULL_ADMIN_REQUIRED" /* FullAdminRequired */,
|
|
2645
|
+
"User account must have full admin privileges"
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}),
|
|
2650
|
+
rxjs.finalize(() => {
|
|
2651
|
+
this.authenticating$.next(false);
|
|
2652
|
+
}),
|
|
2653
|
+
rxjs.take(1)
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* No reconnect token is requested here, though v26+ would mint one: an
|
|
2658
|
+
* api-key session already reconnects without a prompt, since the key is held
|
|
2659
|
+
* and replayed. The token exists for the credential that cannot be.
|
|
2660
|
+
*/
|
|
2661
|
+
loginWithApiKey(credentials) {
|
|
2662
|
+
const { username, key } = credentials;
|
|
2663
|
+
const message = createJsonRpcMessage("auth.login_ex", [
|
|
2664
|
+
{
|
|
2665
|
+
mechanism: "API_KEY_PLAIN" /* ApiKey */,
|
|
2666
|
+
username,
|
|
2667
|
+
api_key: key
|
|
2668
|
+
}
|
|
2669
|
+
]);
|
|
2670
|
+
this.authenticating$.next(true);
|
|
2671
|
+
this.connection.send(message);
|
|
2672
|
+
const messageId = message.id ?? "";
|
|
2673
|
+
return this.connection.messages().pipe(
|
|
2674
|
+
withId(messageId),
|
|
2675
|
+
rxjs.map((msg) => {
|
|
2676
|
+
if (msg.error) {
|
|
2677
|
+
const errorMessage = getApiErrorMessage(
|
|
2678
|
+
msg.error,
|
|
2679
|
+
"Authentication failed"
|
|
2680
|
+
);
|
|
2681
|
+
throw new Error(errorMessage);
|
|
2682
|
+
}
|
|
2683
|
+
return msg.result;
|
|
2684
|
+
}),
|
|
2685
|
+
throwOnAuthenticationFailure(
|
|
2686
|
+
"API_KEY_AUTH_FAILED" /* ApiKeyAuthFailed */,
|
|
2687
|
+
"TrueNAS authentication failed. Has the TrueNAS Connect API key been removed from your TrueNAS server?"
|
|
2688
|
+
),
|
|
2689
|
+
rxjs.tap((res) => {
|
|
2690
|
+
this.credentials.username = username;
|
|
2691
|
+
this.credentials.key = key;
|
|
2692
|
+
this.sessionLifetime = res.user_info?.attributes?.preferences?.lifetime ?? _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2693
|
+
this.authenticated$.next(true);
|
|
2694
|
+
}),
|
|
2695
|
+
rxjs.finalize(() => {
|
|
2696
|
+
this.authenticating$.next(false);
|
|
2697
|
+
}),
|
|
2698
|
+
rxjs.take(1)
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
newApiKey(username) {
|
|
2702
|
+
const message = createJsonRpcMessage("api_key.create", [
|
|
2703
|
+
{ name: `tnc-${randomUUID()}`, username }
|
|
2704
|
+
]);
|
|
2705
|
+
this.connection.send(message);
|
|
2706
|
+
const messageId = message.id ?? "";
|
|
2707
|
+
return this.connection.messages().pipe(
|
|
2708
|
+
withId(messageId),
|
|
2709
|
+
rxjs.map((msg) => {
|
|
2710
|
+
if (msg.error) {
|
|
2711
|
+
const errorMessage = getApiErrorMessage(
|
|
2712
|
+
msg.error,
|
|
2713
|
+
"Failed to create API key"
|
|
2714
|
+
);
|
|
2715
|
+
throw new Error(errorMessage);
|
|
2716
|
+
}
|
|
2717
|
+
return msg.result;
|
|
2718
|
+
}),
|
|
2719
|
+
rxjs.take(1)
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
logout() {
|
|
2723
|
+
const message = createJsonRpcMessage("auth.logout");
|
|
2724
|
+
this.connection.send(message);
|
|
2725
|
+
const messageId = message.id ?? "";
|
|
2726
|
+
return this.connection.messages().pipe(
|
|
2727
|
+
withId(messageId),
|
|
2728
|
+
rxjs.map((msg) => {
|
|
2729
|
+
if (msg.error) {
|
|
2730
|
+
return false;
|
|
2731
|
+
}
|
|
2732
|
+
return msg.result;
|
|
2733
|
+
}),
|
|
2734
|
+
rxjs.tap((success) => {
|
|
2735
|
+
this.sessionLifetime = _TrueNasAuthenticator.DefaultSessionLifetime;
|
|
2736
|
+
this.authenticated$.next(!success);
|
|
2737
|
+
}),
|
|
2738
|
+
rxjs.take(1)
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
// src/logger.ts
|
|
2744
|
+
var consoleLogger = {
|
|
2745
|
+
trace: (message, ...args) => console.trace(message, ...args),
|
|
2746
|
+
debug: (message, ...args) => console.debug(message, ...args),
|
|
2747
|
+
info: (message, ...args) => console.info(message, ...args),
|
|
2748
|
+
warn: (message, ...args) => console.warn(message, ...args),
|
|
2749
|
+
error: (message, ...args) => console.error(message, ...args)
|
|
2750
|
+
};
|
|
2751
|
+
var noop = () => void 0;
|
|
2752
|
+
var noopLogger = {
|
|
2753
|
+
trace: noop,
|
|
2754
|
+
debug: noop,
|
|
2755
|
+
info: noop,
|
|
2756
|
+
warn: noop,
|
|
2757
|
+
error: noop
|
|
2758
|
+
};
|
|
2886
2759
|
|
|
2887
|
-
// src/
|
|
2888
|
-
|
|
2760
|
+
// src/utils/truenas-connection.utils.ts
|
|
2761
|
+
function isHttpStatusError(reason) {
|
|
2762
|
+
return ["404", "502", "503"].some((code) => reason.includes(code));
|
|
2763
|
+
}
|
|
2764
|
+
function getHttpError(reason) {
|
|
2765
|
+
if (reason.includes("404")) {
|
|
2766
|
+
return "API endpoint not found - System may not support versioned API";
|
|
2767
|
+
}
|
|
2768
|
+
if (reason.includes("502")) {
|
|
2769
|
+
return "Bad Gateway - System may be starting up";
|
|
2770
|
+
}
|
|
2771
|
+
if (reason.includes("503")) {
|
|
2772
|
+
return "Service Unavailable - System may be overloaded";
|
|
2773
|
+
}
|
|
2774
|
+
return "";
|
|
2775
|
+
}
|
|
2776
|
+
function getWebSocketError(code) {
|
|
2777
|
+
switch (code) {
|
|
2778
|
+
case 1e3:
|
|
2779
|
+
return "Connection closed normally";
|
|
2780
|
+
case 1001:
|
|
2781
|
+
return "Server is shutting down or restarting";
|
|
2782
|
+
case 1002:
|
|
2783
|
+
return "Protocol error - Invalid WebSocket communication";
|
|
2784
|
+
case 1003:
|
|
2785
|
+
return "Unsupported data format received";
|
|
2786
|
+
case 1006:
|
|
2787
|
+
return "Connection lost unexpectedly - Check network connectivity";
|
|
2788
|
+
case 1007:
|
|
2789
|
+
return "Invalid data received from server";
|
|
2790
|
+
case 1008:
|
|
2791
|
+
return "Connection terminated due to policy violation";
|
|
2792
|
+
case 1009:
|
|
2793
|
+
return "Message size exceeded server limits";
|
|
2794
|
+
case 1011:
|
|
2795
|
+
return "Server encountered an internal error";
|
|
2796
|
+
case 1012:
|
|
2797
|
+
return "Service is restarting";
|
|
2798
|
+
case 1013:
|
|
2799
|
+
return "Server temporarily overloaded - Try again later";
|
|
2800
|
+
case 1015:
|
|
2801
|
+
return "TLS/Certificate error - Certificate may be expired";
|
|
2802
|
+
default:
|
|
2803
|
+
return "Network connection failed - Check IP address and connectivity";
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
var TrueNasSocket = class {
|
|
2807
|
+
constructor(config) {
|
|
2808
|
+
this.config = config;
|
|
2809
|
+
this.socket = webSocket.webSocket(this.config);
|
|
2810
|
+
}
|
|
2811
|
+
messages() {
|
|
2812
|
+
return this.socket;
|
|
2813
|
+
}
|
|
2814
|
+
next(msg) {
|
|
2815
|
+
this.socket.next(msg);
|
|
2816
|
+
}
|
|
2817
|
+
complete() {
|
|
2818
|
+
this.socket.complete();
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
// src/types/transport.type.ts
|
|
2823
|
+
function httpScheme(protocol) {
|
|
2824
|
+
return protocol === "http:" ? "http:" : "https:";
|
|
2825
|
+
}
|
|
2826
|
+
function socketScheme(protocol) {
|
|
2827
|
+
return protocol === "http:" ? "ws:" : "wss:";
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// src/connection/truenas-connection.ts
|
|
2831
|
+
var tenSeconds = 10 * 1e3;
|
|
2832
|
+
var twentySeconds = 20 * 1e3;
|
|
2833
|
+
var TrueNasConnection = class {
|
|
2834
|
+
constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:") {
|
|
2835
|
+
this.hostnames = hostnames;
|
|
2836
|
+
this.systemUuid = systemUuid;
|
|
2837
|
+
this.websocketPath = websocketPath;
|
|
2838
|
+
this.systemName = systemName;
|
|
2839
|
+
this.retryDelay = retryDelay;
|
|
2840
|
+
this.maxRetry = maxRetry;
|
|
2841
|
+
this.logger = logger;
|
|
2842
|
+
this.protocol = protocol;
|
|
2843
|
+
// compatibility properties
|
|
2844
|
+
this.opened = new rxjs.BehaviorSubject(false);
|
|
2845
|
+
this.closed = new rxjs.Subject();
|
|
2846
|
+
this.hostname = new rxjs.BehaviorSubject("");
|
|
2847
|
+
this.connectionAttempts = new rxjs.BehaviorSubject(0);
|
|
2848
|
+
this.lastErrorMessage = new rxjs.BehaviorSubject(null);
|
|
2849
|
+
/**
|
|
2850
|
+
* emits when the connection is manually closed.
|
|
2851
|
+
*/
|
|
2852
|
+
this.closeConnection = new rxjs.Subject();
|
|
2853
|
+
/**
|
|
2854
|
+
* the connection only establishes a socket while this gate is `true`. consumers
|
|
2855
|
+
* flip it via `setEnabled()` (the app maps `SystemState.Active -> true`).
|
|
2856
|
+
*/
|
|
2857
|
+
this.enabled$ = new rxjs.BehaviorSubject(false);
|
|
2858
|
+
/**
|
|
2859
|
+
* observable which emits the current gate value and only emits again when it changes.
|
|
2860
|
+
*/
|
|
2861
|
+
this.enabledChange$ = this.enabled$.pipe(rxjs.distinctUntilChanged());
|
|
2862
|
+
/**
|
|
2863
|
+
* observable that handles the entire connection lifecycle.
|
|
2864
|
+
*/
|
|
2865
|
+
this.connection$ = this.enabledChange$.pipe(
|
|
2866
|
+
rxjs.switchMap((enabled) => {
|
|
2867
|
+
if (enabled) {
|
|
2868
|
+
return this.connect().pipe(
|
|
2869
|
+
rxjs.map((conn) => makeActiveConnection(conn.ws, conn.hostname))
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
return rxjs.of(closedConnection);
|
|
2873
|
+
}),
|
|
2874
|
+
rxjs.switchMap((connection) => {
|
|
2875
|
+
if (connection.state !== "active") {
|
|
2876
|
+
return rxjs.of(connection);
|
|
2877
|
+
}
|
|
2878
|
+
const initMessage = createJsonRpcMessage("core.set_options", [
|
|
2879
|
+
{ legacy_jobs: false }
|
|
2880
|
+
]);
|
|
2881
|
+
connection.ws.next(initMessage);
|
|
2882
|
+
return connection.ws.messages().pipe(
|
|
2883
|
+
rxjs.filter((msg) => msg.id === initMessage.id),
|
|
2884
|
+
rxjs.take(1),
|
|
2885
|
+
rxjs.map(() => connection)
|
|
2886
|
+
);
|
|
2887
|
+
}),
|
|
2888
|
+
// in the event of an error, do two things:
|
|
2889
|
+
// 1. emit an errored connection downstream to inform consumers that
|
|
2890
|
+
// the connection is dead.
|
|
2891
|
+
// 2. re-throw an error so the downstream `retry` will re-subscribe.
|
|
2892
|
+
rxjs.catchError((err) => {
|
|
2893
|
+
this.logger.error("All connections failed - retrying", { message: err?.message, hostname: err?.hostname });
|
|
2894
|
+
return rxjs.concat(
|
|
2895
|
+
rxjs.of(makeConnectionError(err.message, err.hostname)),
|
|
2896
|
+
// since this error will immediately get caught by `retry` there's no reason to build a real error.
|
|
2897
|
+
rxjs.throwError(() => null)
|
|
2898
|
+
);
|
|
2899
|
+
}),
|
|
2900
|
+
rxjs.retry(),
|
|
2901
|
+
// start with a closed connection.
|
|
2902
|
+
rxjs.startWith(closedConnection),
|
|
2903
|
+
// prevent multiple subscriptions from re-evaluating the entire pipeline.
|
|
2904
|
+
rxjs.shareReplay({ bufferSize: 1, refCount: false }),
|
|
2905
|
+
rxjs.takeUntil(this.closeConnection)
|
|
2906
|
+
);
|
|
2907
|
+
/**
|
|
2908
|
+
* observable which is either an instance of the current `TrueNasSocket` used for the connection
|
|
2909
|
+
* or `null` if the connection is closed or errored for any reason.
|
|
2910
|
+
*/
|
|
2911
|
+
this.ws$ = this.connection$.pipe(
|
|
2912
|
+
rxjs.map((conn) => conn.state === "active" ? conn.ws : null)
|
|
2913
|
+
);
|
|
2914
|
+
/**
|
|
2915
|
+
* current hostname we used to build the URL for this connection.
|
|
2916
|
+
*/
|
|
2917
|
+
this.hostname$ = this.connection$.pipe(
|
|
2918
|
+
rxjs.map((conn) => conn.state === "active" ? conn.hostname : null)
|
|
2919
|
+
);
|
|
2920
|
+
/**
|
|
2921
|
+
* emits error messages from the connection pipeline. this is used to derive the
|
|
2922
|
+
* corresponding compatibility property.
|
|
2923
|
+
*/
|
|
2924
|
+
this.lastErrorMessage$ = this.connection$.pipe(
|
|
2925
|
+
rxjs.map((conn) => conn.state === "error" ? conn.message : null)
|
|
2926
|
+
);
|
|
2927
|
+
/**
|
|
2928
|
+
* emits when the connection state *changes*, so it'll never emit `true` twice
|
|
2929
|
+
* in a row nor `false` twice in a row.
|
|
2930
|
+
*/
|
|
2931
|
+
this.opened$ = this.connection$.pipe(
|
|
2932
|
+
rxjs.map((conn) => conn.state),
|
|
2933
|
+
rxjs.distinctUntilChanged(),
|
|
2934
|
+
rxjs.map((state) => state === "active")
|
|
2935
|
+
);
|
|
2936
|
+
/**
|
|
2937
|
+
* emits when the connection state goes from open to closed.
|
|
2938
|
+
*/
|
|
2939
|
+
this.closed$ = this.connection$.pipe(
|
|
2940
|
+
rxjs.map((conn) => conn.state),
|
|
2941
|
+
rxjs.distinctUntilChanged(),
|
|
2942
|
+
rxjs.filter((state) => state === "closed" || state === "error"),
|
|
2943
|
+
rxjs.map(() => {
|
|
2944
|
+
})
|
|
2945
|
+
);
|
|
2946
|
+
/**
|
|
2947
|
+
* whether to display a connection error to the user — the **live** signal.
|
|
2948
|
+
*
|
|
2949
|
+
* `true` only while `connection$` is currently a `ConnectionError`, meaning the entire race +
|
|
2950
|
+
* retry cycle has been exhausted for all hostnames right now; it flips back to `false` once a
|
|
2951
|
+
* connection is re-established. individual socket losses during a race are expected and not surfaced.
|
|
2952
|
+
*
|
|
2953
|
+
* NOTE: this is NOT the same as the `hasExhaustedRetries()` method, which is a **cumulative**
|
|
2954
|
+
* snapshot (see there). The two can disagree — prefer this observable for "is the connection
|
|
2955
|
+
* errored right now?".
|
|
2956
|
+
*/
|
|
2957
|
+
this.hasConnectionError$ = this.connection$.pipe(
|
|
2958
|
+
rxjs.map((conn) => conn.state === "error"),
|
|
2959
|
+
rxjs.takeUntil(this.closeConnection)
|
|
2960
|
+
);
|
|
2961
|
+
/**
|
|
2962
|
+
* observable which always emits messages from the current socket.
|
|
2963
|
+
*/
|
|
2964
|
+
this.messages$ = this.ws$.pipe(
|
|
2965
|
+
rxjs.filter((ws) => ws !== null),
|
|
2966
|
+
rxjs.switchMap((ws) => ws.messages().pipe(
|
|
2967
|
+
rxjs.catchError(() => rxjs.EMPTY)
|
|
2968
|
+
))
|
|
2969
|
+
);
|
|
2970
|
+
rxjs.combineLatest([
|
|
2971
|
+
rxjs.interval(twentySeconds),
|
|
2972
|
+
this.ws$
|
|
2973
|
+
]).pipe(
|
|
2974
|
+
rxjs.takeUntil(this.closeConnection)
|
|
2975
|
+
).subscribe(([, ws]) => {
|
|
2976
|
+
if (ws) {
|
|
2977
|
+
const pingMessage = createJsonRpcMessage("core.ping");
|
|
2978
|
+
ws.next(pingMessage);
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
this.opened$.subscribe((val) => this.opened.next(val));
|
|
2982
|
+
this.closed$.subscribe(() => this.closed.next());
|
|
2983
|
+
this.hostname$.pipe(rxjs.filter(Boolean)).subscribe((name) => this.hostname.next(name));
|
|
2984
|
+
this.ws$.pipe(rxjs.filter(Boolean)).subscribe((ws) => this.ws = ws);
|
|
2985
|
+
this.lastErrorMessage$.subscribe((msg) => this.lastErrorMessage.next(msg));
|
|
2986
|
+
this.enabled$.next(initialEnabled);
|
|
2987
|
+
}
|
|
2889
2988
|
/**
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2892
|
-
*
|
|
2893
|
-
* types for, so the oldest generated version *is* the minimum — there is no
|
|
2894
|
-
* second decision to make, and therefore nothing to keep in sync.
|
|
2989
|
+
* whether the connection has exhausted its retries — the **cumulative** snapshot, read
|
|
2990
|
+
* synchronously. (Formerly `hasConnectionError()`; renamed to disambiguate it from the
|
|
2991
|
+
* live `hasConnectionError$` observable, with which it can disagree.)
|
|
2895
2992
|
*
|
|
2896
|
-
*
|
|
2897
|
-
*
|
|
2898
|
-
* it would duplicate that value with nothing enforcing agreement, so raising
|
|
2899
|
-
* one and forgetting the other would silently keep generating types for
|
|
2900
|
-
* versions the client rejects.
|
|
2993
|
+
* `true` when the lifetime `connectionAttempts` count exceeds `hostnames.length * maxRetry`,
|
|
2994
|
+
* OR when an error message is currently set. Ported from the source (tncui) behavior.
|
|
2901
2995
|
*/
|
|
2902
|
-
|
|
2996
|
+
hasExhaustedRetries() {
|
|
2997
|
+
const attemptsExhausted = this.connectionAttempts.value > this.hostnames.length * this.maxRetry;
|
|
2998
|
+
const hasErrorMessage = this.lastErrorMessage.value !== null;
|
|
2999
|
+
return attemptsExhausted || hasErrorMessage;
|
|
3000
|
+
}
|
|
2903
3001
|
/**
|
|
2904
|
-
*
|
|
2905
|
-
*
|
|
2906
|
-
* NOT derived, deliberately — and for a different reason than it used to
|
|
2907
|
-
* lag. It sat at v26.0.0 while types for v27 already shipped, because
|
|
2908
|
-
* `CLIENT_BY_VERSION_KEY` had no `27` and a v27 system would have passed the
|
|
2909
|
-
* range check only to throw on client selection. `TrueNasApiClientV27` now
|
|
2910
|
-
* exists, so this moves up with it.
|
|
2911
|
-
*
|
|
2912
|
-
* What it must not become is `SUPPORTED_API_VERSIONS[length - 1]`, the mirror
|
|
2913
|
-
* of how MIN is derived. MIN is safe to derive because the oldest generated
|
|
2914
|
-
* version always has a client — it is the floor the clients were built from.
|
|
2915
|
-
* The ceiling is not symmetric: generating types for v28 does not write a v28
|
|
2916
|
-
* client, so deriving this would re-create the exact gap described above on
|
|
2917
|
-
* the next regeneration, silently. The real invariant is "the newest version
|
|
2918
|
-
* a client can be built for", which lives in `CLIENT_BY_VERSION_KEY`; a
|
|
2919
|
-
* factory test asserts the two agree, which is the check that keeps this
|
|
2920
|
-
* literal honest without importing the factory here.
|
|
3002
|
+
* enables or disables the connection gate. the app calls this when its `SystemState`
|
|
3003
|
+
* changes (mapping `SystemState.Active -> true`, everything else -> `false`).
|
|
2921
3004
|
*/
|
|
2922
|
-
|
|
3005
|
+
setEnabled(enabled) {
|
|
3006
|
+
this.enabled$.next(enabled);
|
|
3007
|
+
}
|
|
2923
3008
|
/**
|
|
2924
|
-
*
|
|
2925
|
-
*
|
|
2926
|
-
* the system will attempt to connect using this version as a best-effort fallback.
|
|
2927
|
-
*
|
|
2928
|
-
* WARNING: Status 0 errors have multiple causes:
|
|
2929
|
-
* - CORS policy blocking the request
|
|
2930
|
-
* - Network disconnected
|
|
2931
|
-
* - DNS lookup failure
|
|
2932
|
-
* - Server unreachable
|
|
2933
|
-
*
|
|
2934
|
-
* Using this fallback means systems with genuine network issues will still
|
|
2935
|
-
* attempt connection (and fail during WebSocket handshake instead of immediately).
|
|
3009
|
+
* compatibility method which just returns the public `messages$` observable.
|
|
3010
|
+
* this is used in a few places in the codebase that relied upon the old implementation.
|
|
2936
3011
|
*/
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
// src/types/api-version.type.ts
|
|
2941
|
-
var VersionCompatibility = /* @__PURE__ */ ((VersionCompatibility2) => {
|
|
2942
|
-
VersionCompatibility2["Compatible"] = "compatible";
|
|
2943
|
-
VersionCompatibility2["TooOld"] = "too-old";
|
|
2944
|
-
VersionCompatibility2["TooNew"] = "too-new";
|
|
2945
|
-
VersionCompatibility2["Invalid"] = "invalid";
|
|
2946
|
-
return VersionCompatibility2;
|
|
2947
|
-
})(VersionCompatibility || {});
|
|
2948
|
-
|
|
2949
|
-
// src/utils/api-version.utils.ts
|
|
2950
|
-
var legacyCutoffYear = 25;
|
|
2951
|
-
function parseApiVersion(versionString) {
|
|
2952
|
-
const regex = /^v(\d{2})\.(\d+)\.(\d+)$/;
|
|
2953
|
-
const match = versionString.match(regex);
|
|
2954
|
-
if (!match) {
|
|
2955
|
-
return null;
|
|
2956
|
-
}
|
|
2957
|
-
const [, yearStr, minorStr, patchStr] = match;
|
|
2958
|
-
const year = parseInt(yearStr, 10);
|
|
2959
|
-
const minor = parseInt(minorStr, 10);
|
|
2960
|
-
const patch = parseInt(patchStr, 10);
|
|
2961
|
-
if (year <= legacyCutoffYear) {
|
|
2962
|
-
if (minor < 1 || minor > 12) {
|
|
2963
|
-
return null;
|
|
2964
|
-
}
|
|
2965
|
-
} else if (year >= 26) {
|
|
2966
|
-
if (minor < 0 || minor > 99) {
|
|
2967
|
-
return null;
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
return {
|
|
2971
|
-
version: versionString,
|
|
2972
|
-
year,
|
|
2973
|
-
minor,
|
|
2974
|
-
patch,
|
|
2975
|
-
websocketPath: `/api/${versionString}`
|
|
2976
|
-
};
|
|
2977
|
-
}
|
|
2978
|
-
function compareVersions(a, b) {
|
|
2979
|
-
if (a.year !== b.year) {
|
|
2980
|
-
return a.year - b.year;
|
|
3012
|
+
messages() {
|
|
3013
|
+
return this.messages$;
|
|
2981
3014
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
3015
|
+
/**
|
|
3016
|
+
* sends a message over the current websocket OR queues it to send once
|
|
3017
|
+
* the next websocket is opened. messages sent via `send` will not be lost until after
|
|
3018
|
+
* the next `closed$` emission.
|
|
3019
|
+
*/
|
|
3020
|
+
send(message) {
|
|
3021
|
+
return this.ws$.pipe(
|
|
3022
|
+
rxjs.filter((ws) => ws !== null),
|
|
3023
|
+
rxjs.take(1)
|
|
3024
|
+
).subscribe((ws) => ws.next(message));
|
|
2984
3025
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
return "invalid" /* Invalid */;
|
|
3026
|
+
/**
|
|
3027
|
+
* manually closes this connection and prevents it from retrying/opening any more.
|
|
3028
|
+
*/
|
|
3029
|
+
close() {
|
|
3030
|
+
this.closeConnection.next();
|
|
3031
|
+
this.closeConnection.complete();
|
|
2992
3032
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
3033
|
+
/**
|
|
3034
|
+
* helper method which creates an `Observable` which emits an `ActiveConnection`
|
|
3035
|
+
* after it establishes a connection to the given hostname. the observable will emit an
|
|
3036
|
+
* error if the connection is never established and will not complete until unsubscribed from or closed.
|
|
3037
|
+
*/
|
|
3038
|
+
createSocket(hostname) {
|
|
3039
|
+
const url = `${socketScheme(this.protocol)}//${hostname}${this.websocketPath}`;
|
|
3040
|
+
let hasOpened = false;
|
|
3041
|
+
return new rxjs.Observable((subscriber) => {
|
|
3042
|
+
const ws = new TrueNasSocket({
|
|
3043
|
+
url,
|
|
3044
|
+
openObserver: {
|
|
3045
|
+
next: () => {
|
|
3046
|
+
hasOpened = true;
|
|
3047
|
+
this.connectionAttempts.next(0);
|
|
3048
|
+
subscriber.next({ ws, hostname });
|
|
3049
|
+
}
|
|
3050
|
+
},
|
|
3051
|
+
closeObserver: {
|
|
3052
|
+
next: (event) => {
|
|
3053
|
+
const reason = event.reason || "";
|
|
3054
|
+
let errorMessage;
|
|
3055
|
+
if (isHttpStatusError(reason)) {
|
|
3056
|
+
errorMessage = getHttpError(reason);
|
|
3057
|
+
} else {
|
|
3058
|
+
errorMessage = getWebSocketError(event.code);
|
|
3059
|
+
}
|
|
3060
|
+
this.connectionAttempts.next(this.connectionAttempts.value + 1);
|
|
3061
|
+
subscriber.error(makeConnectionError(errorMessage, hostname));
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
});
|
|
3065
|
+
const startupSub = ws.socket.subscribe({
|
|
3066
|
+
error: () => {
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
3069
|
+
return () => {
|
|
3070
|
+
startupSub.unsubscribe();
|
|
3071
|
+
ws.complete();
|
|
3072
|
+
};
|
|
3073
|
+
}).pipe(
|
|
3074
|
+
// retry logic:
|
|
3075
|
+
// * if a connection is not established in 10 seconds, consider that an error
|
|
3076
|
+
rxjs.timeout({ first: tenSeconds }),
|
|
3077
|
+
rxjs.retry({
|
|
3078
|
+
// * while still *establishing*: wait `retryDelay` before trying again, giving up
|
|
3079
|
+
// after `maxRetry` retries.
|
|
3080
|
+
count: this.maxRetry,
|
|
3081
|
+
// * we use a custom `delay` function here to ensure no retries are performed
|
|
3082
|
+
// once a socket is *opened*.
|
|
3083
|
+
// basically: a later death must propagate out rather than being retried
|
|
3084
|
+
// so it can be handled by the `connection$` observable.
|
|
3085
|
+
delay: (error) => {
|
|
3086
|
+
if (hasOpened) {
|
|
3087
|
+
return rxjs.throwError(() => error);
|
|
3088
|
+
}
|
|
3089
|
+
return rxjs.timer(this.retryDelay);
|
|
3090
|
+
}
|
|
3091
|
+
})
|
|
3092
|
+
);
|
|
2995
3093
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
3094
|
+
/**
|
|
3095
|
+
* helper function which actually performs the parallel connection `race`.
|
|
3096
|
+
*/
|
|
3097
|
+
connect() {
|
|
3098
|
+
return rxjs.race(
|
|
3099
|
+
this.hostnames.map(this.createSocket.bind(this))
|
|
3100
|
+
).pipe(
|
|
3101
|
+
rxjs.tap((conn) => this.logger.debug(`TrueNas socket opened to ${conn.hostname}.`)),
|
|
3102
|
+
rxjs.takeUntil(this.closeConnection)
|
|
3103
|
+
);
|
|
2998
3104
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3105
|
+
};
|
|
3106
|
+
var makeActiveConnection = (ws, hostname) => ({
|
|
3107
|
+
ws,
|
|
3108
|
+
hostname,
|
|
3109
|
+
state: "active"
|
|
3110
|
+
});
|
|
3111
|
+
var makeConnectionError = (message, hostname) => ({
|
|
3112
|
+
name: "ConnectionError",
|
|
3113
|
+
message,
|
|
3114
|
+
hostname,
|
|
3115
|
+
state: "error"
|
|
3116
|
+
});
|
|
3117
|
+
var closedConnection = { state: "closed" };
|
|
3010
3118
|
|
|
3011
3119
|
// src/client/truenas-api-client.ts
|
|
3012
3120
|
var TrueNasApiClient = class {
|
|
@@ -3076,7 +3184,7 @@ var TrueNasApiClient = class {
|
|
|
3076
3184
|
* Override in subclasses to provide version-specific authentication.
|
|
3077
3185
|
*/
|
|
3078
3186
|
createAuthenticator() {
|
|
3079
|
-
return new TrueNasAuthenticator(this.connection);
|
|
3187
|
+
return new TrueNasAuthenticator(this.connection, this.version);
|
|
3080
3188
|
}
|
|
3081
3189
|
};
|
|
3082
3190
|
|