@truenas/api-client 4.0.0 → 4.0.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/README.md +59 -0
- package/dist/index.cjs +182 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +228 -17
- package/dist/index.d.ts +228 -17
- package/dist/index.js +182 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14509,6 +14509,103 @@ interface ContainerRestartOptions {
|
|
|
14509
14509
|
force: boolean;
|
|
14510
14510
|
}
|
|
14511
14511
|
|
|
14512
|
+
/**
|
|
14513
|
+
* Types for the version-agnostic `ops.smbStatus` operation.
|
|
14514
|
+
*
|
|
14515
|
+
* `smb.status` is one method with two different standings. On v26+ it is a
|
|
14516
|
+
* public, role-gated endpoint (`SHARING_SMB_READ`) and appears in the generated
|
|
14517
|
+
* directory. On v25.10 it is declared `private=True`, so `--dump-api` never
|
|
14518
|
+
* emitted it and no generated v25.10 type describes it — but the method is
|
|
14519
|
+
* there, under the same name, taking the same four positional arguments and
|
|
14520
|
+
* returning the same union. These types describe that shared wire contract
|
|
14521
|
+
* once, so the operation can present a single shape on every version.
|
|
14522
|
+
*
|
|
14523
|
+
* They are deliberately *not* re-exports of the generated v26 types. Those
|
|
14524
|
+
* describe one version's dump; this describes the contract both versions
|
|
14525
|
+
* happen to honour, and the two are only equal for as long as they stay equal.
|
|
14526
|
+
*/
|
|
14527
|
+
/**
|
|
14528
|
+
* The kind of status being asked for.
|
|
14529
|
+
*
|
|
14530
|
+
* Neither version offers a seventh, but for two different reasons. v25.10
|
|
14531
|
+
* carries a legacy `AUTH_LOG` in its enum and its implementation — it routes to
|
|
14532
|
+
* `audit.query` rather than to `smbstatus` — while its own argument model
|
|
14533
|
+
* refuses it: the `Literal` there lists exactly these six. It is unreachable
|
|
14534
|
+
* even from middleware's own internal calls, because `api_method` validates
|
|
14535
|
+
* every invocation. v26+ does not have it at all; the level was deleted rather
|
|
14536
|
+
* than left dormant.
|
|
14537
|
+
*/
|
|
14538
|
+
type SmbStatusInfoLevel = 'ALL' | 'SESSIONS' | 'SHARES' | 'LOCKS' | 'BYTERANGE' | 'NOTIFICATIONS';
|
|
14539
|
+
/**
|
|
14540
|
+
* Server-side options for the status query itself.
|
|
14541
|
+
*
|
|
14542
|
+
* Field names are snake_case because this object is passed to middleware
|
|
14543
|
+
* verbatim; it is the wire shape, not a translation of it.
|
|
14544
|
+
*
|
|
14545
|
+
* Every field is optional here, matching the generated v26 type. Middleware
|
|
14546
|
+
* defaults them on both versions — `verbose` and `resolve_uids` to true,
|
|
14547
|
+
* `fast` to false, the two `restrict_*` to empty strings.
|
|
14548
|
+
*/
|
|
14549
|
+
interface SmbStatusOptions {
|
|
14550
|
+
verbose?: boolean;
|
|
14551
|
+
fast?: boolean;
|
|
14552
|
+
restrict_user?: string;
|
|
14553
|
+
restrict_session?: string;
|
|
14554
|
+
resolve_uids?: boolean;
|
|
14555
|
+
}
|
|
14556
|
+
/**
|
|
14557
|
+
* Rows returned by `smb.status` are not modelled by either version.
|
|
14558
|
+
*
|
|
14559
|
+
* The middleware result model is `list[dict] | dict | int` on both, and the
|
|
14560
|
+
* dump reproduces that as a `Record<string, unknown>` — the shape genuinely
|
|
14561
|
+
* varies with `info_level`, and neither version declares the per-level
|
|
14562
|
+
* structures. Callers narrow it themselves.
|
|
14563
|
+
*/
|
|
14564
|
+
type SmbStatusRow = Record<string, unknown>;
|
|
14565
|
+
/**
|
|
14566
|
+
* What `smb.status` may return.
|
|
14567
|
+
*
|
|
14568
|
+
* The three arms are not interchangeable and which one arrives is decided by
|
|
14569
|
+
* the options, not by the info level: `{ count: true }` gives the number,
|
|
14570
|
+
* `{ get: true }` a single row, anything else the list. This is the same
|
|
14571
|
+
* polymorphism the query verbs exist to resolve, but `smb.status` is not a
|
|
14572
|
+
* `.query` method — the generator did not mark it with an `entity` — so the
|
|
14573
|
+
* union is returned as-is and the caller narrows.
|
|
14574
|
+
*/
|
|
14575
|
+
type SmbStatusResponse = SmbStatusRow[] | SmbStatusRow | number;
|
|
14576
|
+
/**
|
|
14577
|
+
* Arguments for {@link OperationMappings.smbStatus}.
|
|
14578
|
+
*
|
|
14579
|
+
* A single object rather than four positional parameters: the wire form is
|
|
14580
|
+
* positional and every argument is optional, so a caller wanting only
|
|
14581
|
+
* `statusOptions` — the fourth — would otherwise have to pass three
|
|
14582
|
+
* placeholder arguments to reach it. The operation maps this back onto the
|
|
14583
|
+
* positional array.
|
|
14584
|
+
*/
|
|
14585
|
+
interface SmbStatusRequest {
|
|
14586
|
+
/** Defaults to `ALL` on both versions. */
|
|
14587
|
+
infoLevel?: SmbStatusInfoLevel;
|
|
14588
|
+
/**
|
|
14589
|
+
* Query filters applied to the rows.
|
|
14590
|
+
*
|
|
14591
|
+
* Both versions special-case a lone `uid` or `session_id` equality filter,
|
|
14592
|
+
* turning it into the corresponding `restrict_*` status option instead of
|
|
14593
|
+
* filtering after the fact.
|
|
14594
|
+
*
|
|
14595
|
+
* That *overwrites* the status option rather than combining with it. Passing
|
|
14596
|
+
* `filters: [['uid', '=', 1000]]` together with
|
|
14597
|
+
* `statusOptions: { restrict_user: 'bob' }` sends both, and middleware
|
|
14598
|
+
* discards `'bob'`. This is middleware's behaviour on both versions, carried
|
|
14599
|
+
* through unchanged — but the two look like they compose and do not, so do
|
|
14600
|
+
* not set both.
|
|
14601
|
+
*/
|
|
14602
|
+
filters?: QueryFilters<SmbStatusRow>;
|
|
14603
|
+
/** Query options. `{ count: true }` is what makes the result a number. */
|
|
14604
|
+
options?: QueryOptions<SmbStatusRow>;
|
|
14605
|
+
/** Options for the status command itself. */
|
|
14606
|
+
statusOptions?: SmbStatusOptions;
|
|
14607
|
+
}
|
|
14608
|
+
|
|
14512
14609
|
/**
|
|
14513
14610
|
* OperationMappings Interface
|
|
14514
14611
|
*
|
|
@@ -14594,6 +14691,64 @@ interface OperationMappings {
|
|
|
14594
14691
|
* ignored, because `recursive` destroys data that cannot be recovered.
|
|
14595
14692
|
*/
|
|
14596
14693
|
containerDelete: (id: string, options?: ContainerDeleteOptions$1) => Observable<Job | null>;
|
|
14694
|
+
/**
|
|
14695
|
+
* Read SMB server status — sessions, shares, locks, notifications.
|
|
14696
|
+
*
|
|
14697
|
+
* - v25.10: `smb.status`, which is declared `private=True` there
|
|
14698
|
+
* - v26+: `smb.status`, public and gated on the `SHARING_SMB_READ` role
|
|
14699
|
+
*
|
|
14700
|
+
* The same method under the same name on both, taking the same four
|
|
14701
|
+
* positional arguments and returning the same `list | dict | int` union. That
|
|
14702
|
+
* is why this reads as one operation rather than a v26 feature with a v25.10
|
|
14703
|
+
* excuse, and why v25.10 returns real data instead of throwing.
|
|
14704
|
+
*
|
|
14705
|
+
* **The two versions are not equally reachable, though.** Being private on
|
|
14706
|
+
* v25.10 is not only a statement about documentation — it decides
|
|
14707
|
+
* authorization too, and this is the one respect in which the operation
|
|
14708
|
+
* cannot present a single face:
|
|
14709
|
+
*
|
|
14710
|
+
* - **v26+** declares `roles=['SHARING_SMB_READ']`, so any session holding
|
|
14711
|
+
* that role may call it.
|
|
14712
|
+
* - **v25.10** declares no roles at all. Middleware registers a method with
|
|
14713
|
+
* the role manager only `if roles:`, so `smb.status` is in no role's
|
|
14714
|
+
* allowlist, and `authorize('CALL', 'smb.status')` fails for a role-scoped
|
|
14715
|
+
* session with `Not authorized` (`EACCES`). A session holding exactly
|
|
14716
|
+
* `SHARING_SMB_READ` — the role that works on v26+ — is refused on v25.10.
|
|
14717
|
+
* - What does reach it is a **non-STIG full-admin** session, whose allowlist
|
|
14718
|
+
* is the wildcard `{ method: '*', resource: '*' }`. Under STIG, full admin
|
|
14719
|
+
* is expanded to the union of its roles' allowlists instead of the
|
|
14720
|
+
* wildcard, and `smb.status` is in none of them — so STIG full admin is
|
|
14721
|
+
* refused on v25.10 as well.
|
|
14722
|
+
*
|
|
14723
|
+
* Nothing here pre-empts that. The operation does not inspect the session or
|
|
14724
|
+
* refuse ahead of the server; a caller lacking the authority gets
|
|
14725
|
+
* middleware's own error, on the version where it applies.
|
|
14726
|
+
*
|
|
14727
|
+
* It is the first operation whose v25.10 leg is invisible to `--dump-api`: no
|
|
14728
|
+
* generated v25.10 type describes `smb.status`, because the dump omits
|
|
14729
|
+
* private methods. The v25.10 client therefore asserts the method's existence
|
|
14730
|
+
* rather than reading it from the directory, and that assertion is checked
|
|
14731
|
+
* against middleware source, not against the dump. See
|
|
14732
|
+
* `TrueNasApiClientV2510.createOperations`.
|
|
14733
|
+
*
|
|
14734
|
+
* Calling a private method logs a warning server-side on every invocation.
|
|
14735
|
+
* That is accepted for v25.10, which is in maintenance; it is not a pattern
|
|
14736
|
+
* to extend to versions still taking features.
|
|
14737
|
+
*
|
|
14738
|
+
* A client count is this composed rather than given its own operation, since
|
|
14739
|
+
* it is the same endpoint with different options:
|
|
14740
|
+
*
|
|
14741
|
+
* ```typescript
|
|
14742
|
+
* truenas.ops
|
|
14743
|
+
* .smbStatus({
|
|
14744
|
+
* infoLevel: 'SESSIONS',
|
|
14745
|
+
* options: { count: true },
|
|
14746
|
+
* statusOptions: { fast: true },
|
|
14747
|
+
* })
|
|
14748
|
+
* .subscribe((count) => console.log(count as number));
|
|
14749
|
+
* ```
|
|
14750
|
+
*/
|
|
14751
|
+
smbStatus: (request?: SmbStatusRequest) => Observable<SmbStatusResponse>;
|
|
14597
14752
|
}
|
|
14598
14753
|
|
|
14599
14754
|
/**
|
|
@@ -30429,9 +30584,10 @@ interface CreateClientOptions {
|
|
|
30429
30584
|
*
|
|
30430
30585
|
* Omitting it against a plaintext appliance is the quieter failure and the
|
|
30431
30586
|
* likelier one, since it is the case this option exists for. Discovery tries
|
|
30432
|
-
* `https
|
|
30433
|
-
* v25.10.0 has on `/api/versions
|
|
30434
|
-
*
|
|
30587
|
+
* `https` and `fetch` rejects, which reads exactly like the CORS block
|
|
30588
|
+
* v25.10.0 has on `/api/versions`. The reachability probe now runs on the same
|
|
30589
|
+
* scheme and fails the same way, so the mismatch is reported as unreachable
|
|
30590
|
+
* rather than quietly answered with a v25.10.0 client.
|
|
30435
30591
|
*/
|
|
30436
30592
|
protocol?: ApplianceProtocol;
|
|
30437
30593
|
}
|
|
@@ -30545,11 +30701,18 @@ interface CreateClientOptions {
|
|
|
30545
30701
|
* @returns a Promise that resolves with the created client, or rejects with a
|
|
30546
30702
|
* {@link VersionDiscoveryError} subclass (or a client-selection error).
|
|
30547
30703
|
* Rejects if version discovery on all hostnames *fails* and is not recoverable.
|
|
30548
|
-
*
|
|
30549
|
-
*
|
|
30550
|
-
*
|
|
30551
|
-
*
|
|
30552
|
-
*
|
|
30704
|
+
*
|
|
30705
|
+
* A `VersionDiscoveryNetworkError` is not by itself a verdict: `fetch` reports
|
|
30706
|
+
* a CORS refusal, an absent appliance, a bad name and the wrong scheme
|
|
30707
|
+
* identically. So that failure opens a reachability probe and a second
|
|
30708
|
+
* discovery attempt. A retry that succeeds is used; one that fails with a
|
|
30709
|
+
* specific error raises that instead; an appliance that answers nothing
|
|
30710
|
+
* rejects with the network error. Only one that answers the probe and still
|
|
30711
|
+
* will not serve `/api/versions` — which is what 25.10.0 looks like from a
|
|
30712
|
+
* browser — falls back to `FALLBACK_VERSION`.
|
|
30713
|
+
*
|
|
30714
|
+
* A network error alongside a version-compatibility error or a 404 does
|
|
30715
|
+
* *not* reach any of that — see `selectRepresentativeFailure`.
|
|
30553
30716
|
*/
|
|
30554
30717
|
declare function createTrueNasClient<V extends SupportedApiVersion>(opts: CreateClientOptions & {
|
|
30555
30718
|
version: V;
|
|
@@ -30582,6 +30745,12 @@ declare function createTrueNasClient<D extends ApiDirectoryShape = DefaultApiDir
|
|
|
30582
30745
|
* - containerStop → virt.instance.stop (emits Job updates)
|
|
30583
30746
|
* - containerRestart → virt.instance.restart (emits Job updates)
|
|
30584
30747
|
* - containerDelete → virt.instance.delete (already a job; takes no options)
|
|
30748
|
+
*
|
|
30749
|
+
* SMB operations:
|
|
30750
|
+
* - smbStatus → smb.status, which is declared `private=True` on this version
|
|
30751
|
+
* and so is absent from the generated directory. See `PrivateSmbStatusCall`
|
|
30752
|
+
* for how it is reached and why that is a cast, and `OperationMappings` for
|
|
30753
|
+
* the authorization difference being private carries with it here.
|
|
30585
30754
|
*/
|
|
30586
30755
|
declare class TrueNasApiClientV2510 extends TrueNasApiClient<ApiDirectory$7> {
|
|
30587
30756
|
/**
|
|
@@ -30620,6 +30789,9 @@ declare class TrueNasApiClientV2510 extends TrueNasApiClient<ApiDirectory$7> {
|
|
|
30620
30789
|
* - containerStop → container.stop (emits Job updates)
|
|
30621
30790
|
* - containerRestart → container.stop + container.start (emits Job, then null)
|
|
30622
30791
|
* - containerDelete → container.delete (a job since v26.0.0; force/recursive)
|
|
30792
|
+
*
|
|
30793
|
+
* SMB operations:
|
|
30794
|
+
* - smbStatus → smb.status, public here and gated on `SHARING_SMB_READ`
|
|
30623
30795
|
*/
|
|
30624
30796
|
declare class TrueNasApiClientV26 extends TrueNasApiClient<ApiDirectory$1> {
|
|
30625
30797
|
/**
|
|
@@ -30669,10 +30841,26 @@ declare class TrueNasApiClientV26 extends TrueNasApiClient<ApiDirectory$1> {
|
|
|
30669
30841
|
* - containerRestart → container.stop + container.start (emits Job, then null)
|
|
30670
30842
|
* - containerDelete → container.delete (a job since v26.0.0; force/recursive)
|
|
30671
30843
|
*
|
|
30672
|
-
*
|
|
30673
|
-
*
|
|
30674
|
-
* rather than
|
|
30675
|
-
*
|
|
30844
|
+
* SMB operations:
|
|
30845
|
+
* - smbStatus → smb.status, public and gated on `SHARING_SMB_READ`, as v26.
|
|
30846
|
+
* v27 inherits the entry rather than re-declaring it.
|
|
30847
|
+
*
|
|
30848
|
+
* All five container operations are currently identical to v26's, because v27
|
|
30849
|
+
* inherits the container entries the facade touches rather than re-declaring
|
|
30850
|
+
* them. Five operations, four entries — `containerRestart` has none of its own,
|
|
30851
|
+
* being `container.stop` then `container.start`. Asserted rather than assumed,
|
|
30852
|
+
* and re-runnable: the spec pins all four entries — `container.query`,
|
|
30853
|
+
* `.start`, `.stop`, `.delete` — against v26's, so the day they diverge is a
|
|
30854
|
+
* failure there.
|
|
30855
|
+
*
|
|
30856
|
+
* `smbStatus` is not among them. Its v27 entry is inherited from v26 too, but
|
|
30857
|
+
* nothing pins it the way the container entries are pinned. `tsc` is a partial
|
|
30858
|
+
* stand-in and worth stating precisely: this leg reads the method out of the
|
|
30859
|
+
* directory, so a v27 entry that *narrowed* the arguments or widened the
|
|
30860
|
+
* response past `SmbStatusResponse` would stop compiling here. The reverse
|
|
30861
|
+
* would not. A widened `info_level` or a narrowed response still compiles, and
|
|
30862
|
+
* the operation would go on advertising the stale union. Add an `Identical<>`
|
|
30863
|
+
* assertion beside the container ones if that matters.
|
|
30676
30864
|
*
|
|
30677
30865
|
* They are written out here rather than shared with v26 because that is what
|
|
30678
30866
|
* this repo's one-client-per-series design is for: the two are the same today
|
|
@@ -30680,8 +30868,9 @@ declare class TrueNasApiClientV26 extends TrueNasApiClient<ApiDirectory$1> {
|
|
|
30680
30868
|
* the chained stop+start below is the workaround for its absence. Factoring the
|
|
30681
30869
|
* bodies into a common base would couple two versions that exist in order to
|
|
30682
30870
|
* evolve apart, and the coupling would have to be undone by whichever release
|
|
30683
|
-
* diverges first. The `@/generated` types are
|
|
30684
|
-
* silently
|
|
30871
|
+
* diverges first. The `@/generated` types are what guard against them drifting
|
|
30872
|
+
* silently — though only as far as the paragraph above says: a directory change
|
|
30873
|
+
* this code cannot absorb stops compiling here, and one it can absorb does not.
|
|
30685
30874
|
*/
|
|
30686
30875
|
declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
30687
30876
|
/**
|
|
@@ -30694,6 +30883,13 @@ declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
|
30694
30883
|
protected createOperations(): OperationMappings;
|
|
30695
30884
|
}
|
|
30696
30885
|
|
|
30886
|
+
/**
|
|
30887
|
+
* What a reachability probe found. Three states rather than two: "we asked and
|
|
30888
|
+
* got silence" and "there was no way to ask" refuse the CORS fallback for
|
|
30889
|
+
* different reasons, and collapsing them into one `false` is the conflation
|
|
30890
|
+
* this whole change exists to undo.
|
|
30891
|
+
*/
|
|
30892
|
+
type Reachability = 'reachable' | 'silent' | 'cannot-ask';
|
|
30697
30893
|
/**
|
|
30698
30894
|
* Discovers available API versions from TrueNAS systems.
|
|
30699
30895
|
*
|
|
@@ -30708,8 +30904,10 @@ declare class TrueNasApiClientV27 extends TrueNasApiClient<ApiDirectory> {
|
|
|
30708
30904
|
* service. Because `fetch` resolves (rather than rejects) on non-2xx responses and
|
|
30709
30905
|
* throws a `TypeError` on network/CORS/unreachable failures, the error contract
|
|
30710
30906
|
* differs from the original: a network/CORS/unreachable failure surfaces as a
|
|
30711
|
-
* {@link VersionDiscoveryNetworkError}
|
|
30712
|
-
*
|
|
30907
|
+
* {@link VersionDiscoveryNetworkError} (replacing the old
|
|
30908
|
+
* `HttpErrorResponse.status === 0` check). That error names a symptom with
|
|
30909
|
+
* several causes, so it opens the client factory's disambiguation rather than
|
|
30910
|
+
* deciding it — see {@link VersionDiscovery.probeReachable}.
|
|
30713
30911
|
*/
|
|
30714
30912
|
declare class VersionDiscovery {
|
|
30715
30913
|
private readonly logger;
|
|
@@ -30717,6 +30915,19 @@ declare class VersionDiscovery {
|
|
|
30717
30915
|
private versionCache;
|
|
30718
30916
|
constructor(logger?: Logger, protocol?: ApplianceProtocol);
|
|
30719
30917
|
private versionsUrl;
|
|
30918
|
+
/**
|
|
30919
|
+
* Whether the appliance answers at all, asked in a way CORS cannot block.
|
|
30920
|
+
*
|
|
30921
|
+
* A `no-cors` request yields a response the page may not read, but its
|
|
30922
|
+
* *arrival* is the whole answer here: something served it. That separates the
|
|
30923
|
+
* two failures `fetch` reports identically — a box that refused to share its
|
|
30924
|
+
* versions with this origin, and a box that is not there.
|
|
30925
|
+
*
|
|
30926
|
+
* `cannot-ask` where CORS is not enforced: nothing is blocking anything, so a
|
|
30927
|
+
* failed fetch already means unreachable and a probe would only repeat what
|
|
30928
|
+
* discovery just found. It is not a quieter way of saying "no".
|
|
30929
|
+
*/
|
|
30930
|
+
probeReachable(hostname: string): Promise<Reachability>;
|
|
30720
30931
|
/**
|
|
30721
30932
|
* Discovers the API version for a given hostname.
|
|
30722
30933
|
*
|
|
@@ -30912,4 +31123,4 @@ type ApiError = JsonRpcError | TrueNasError;
|
|
|
30912
31123
|
*/
|
|
30913
31124
|
declare function getApiErrorMessage(error: unknown, fallback?: string): string;
|
|
30914
31125
|
|
|
30915
|
-
export { type ApiCallDirectory$7 as ApiCallDirectoryV25_10_0, type ApiCallDirectory$6 as ApiCallDirectoryV25_10_1, type ApiCallDirectory$5 as ApiCallDirectoryV25_10_2, type ApiCallDirectory$4 as ApiCallDirectoryV25_10_3, type ApiCallDirectory$3 as ApiCallDirectoryV25_10_4, type ApiCallDirectory$2 as ApiCallDirectoryV25_10_5, type ApiCallDirectory$1 as ApiCallDirectoryV26_0_0, type ApiCallDirectory as ApiCallDirectoryV27_0_0, type ApiDirectoryByVersion, type ApiDirectoryShape, type ApiDirectory$7 as ApiDirectoryV25_10_0, type ApiDirectory$6 as ApiDirectoryV25_10_1, type ApiDirectory$5 as ApiDirectoryV25_10_2, type ApiDirectory$4 as ApiDirectoryV25_10_3, type ApiDirectory$3 as ApiDirectoryV25_10_4, type ApiDirectory$2 as ApiDirectoryV25_10_5, type ApiDirectory$1 as ApiDirectoryV26_0_0, type ApiDirectory as ApiDirectoryV27_0_0, type ApiError, type ApiEventDirectory$7 as ApiEventDirectoryV25_10_0, type ApiEventDirectory$6 as ApiEventDirectoryV25_10_1, type ApiEventDirectory$5 as ApiEventDirectoryV25_10_2, type ApiEventDirectory$4 as ApiEventDirectoryV25_10_3, type ApiEventDirectory$3 as ApiEventDirectoryV25_10_4, type ApiEventDirectory$2 as ApiEventDirectoryV25_10_5, type ApiEventDirectory$1 as ApiEventDirectoryV26_0_0, type ApiEventDirectory as ApiEventDirectoryV27_0_0, type ApiJobDirectory$7 as ApiJobDirectoryV25_10_0, type ApiJobDirectory$6 as ApiJobDirectoryV25_10_1, type ApiJobDirectory$5 as ApiJobDirectoryV25_10_2, type ApiJobDirectory$4 as ApiJobDirectoryV25_10_3, type ApiJobDirectory$3 as ApiJobDirectoryV25_10_4, type ApiJobDirectory$2 as ApiJobDirectoryV25_10_5, type ApiJobDirectory$1 as ApiJobDirectoryV26_0_0, type ApiJobDirectory as ApiJobDirectoryV27_0_0, type ApiKeyCreate, type ApiVersion, type ApiVersionResponse, AppState, type ApplianceProtocol, type ArgsOf, AuthError, AuthErrorCode, type AuthResponse, type BaseApiDirectory, type CallMethod, type CallParams, type CallResponse, type Container, type CreateClientOptions, type DefaultApiDirectory, type EventKind, type EventName, type EventUnion, InvalidVersionResponseError, type Job, type JobMethod, type JobParams, type JobProgress, type JobResult, JobState, type Logger, NoCompatibleVersionsError, type OperationMappings, type QueryDirectory, type QueryEntity, type QueryListOptions, type QueryMethod, type QuerySingleOptions, SUPPORTED_API_VERSIONS, type SupportedApiVersion, TrueNasApiClient, TrueNasApiClientV2510, TrueNasApiClientV26, TrueNasApiClientV27, TrueNasAuthMechanism, type TrueNasDate, UserRole, type UserRoleName, VersionCompatibility, VersionDiscovery, VersionDiscoveryError, VersionDiscoveryNetworkError, VersionDiscoveryTimeoutError, VersionEndpointNotFoundError, VersionTooNewError, VersionTooOldError, consoleLogger, createTrueNasClient, getApiErrorMessage, isJobFinished, noopLogger, index$7 as v25_10_0, index$6 as v25_10_1, index$5 as v25_10_2, index$4 as v25_10_3, index$3 as v25_10_4, index$2 as v25_10_5, index$1 as v26_0_0, index as v27_0_0 };
|
|
31126
|
+
export { type ApiCallDirectory$7 as ApiCallDirectoryV25_10_0, type ApiCallDirectory$6 as ApiCallDirectoryV25_10_1, type ApiCallDirectory$5 as ApiCallDirectoryV25_10_2, type ApiCallDirectory$4 as ApiCallDirectoryV25_10_3, type ApiCallDirectory$3 as ApiCallDirectoryV25_10_4, type ApiCallDirectory$2 as ApiCallDirectoryV25_10_5, type ApiCallDirectory$1 as ApiCallDirectoryV26_0_0, type ApiCallDirectory as ApiCallDirectoryV27_0_0, type ApiDirectoryByVersion, type ApiDirectoryShape, type ApiDirectory$7 as ApiDirectoryV25_10_0, type ApiDirectory$6 as ApiDirectoryV25_10_1, type ApiDirectory$5 as ApiDirectoryV25_10_2, type ApiDirectory$4 as ApiDirectoryV25_10_3, type ApiDirectory$3 as ApiDirectoryV25_10_4, type ApiDirectory$2 as ApiDirectoryV25_10_5, type ApiDirectory$1 as ApiDirectoryV26_0_0, type ApiDirectory as ApiDirectoryV27_0_0, type ApiError, type ApiEventDirectory$7 as ApiEventDirectoryV25_10_0, type ApiEventDirectory$6 as ApiEventDirectoryV25_10_1, type ApiEventDirectory$5 as ApiEventDirectoryV25_10_2, type ApiEventDirectory$4 as ApiEventDirectoryV25_10_3, type ApiEventDirectory$3 as ApiEventDirectoryV25_10_4, type ApiEventDirectory$2 as ApiEventDirectoryV25_10_5, type ApiEventDirectory$1 as ApiEventDirectoryV26_0_0, type ApiEventDirectory as ApiEventDirectoryV27_0_0, type ApiJobDirectory$7 as ApiJobDirectoryV25_10_0, type ApiJobDirectory$6 as ApiJobDirectoryV25_10_1, type ApiJobDirectory$5 as ApiJobDirectoryV25_10_2, type ApiJobDirectory$4 as ApiJobDirectoryV25_10_3, type ApiJobDirectory$3 as ApiJobDirectoryV25_10_4, type ApiJobDirectory$2 as ApiJobDirectoryV25_10_5, type ApiJobDirectory$1 as ApiJobDirectoryV26_0_0, type ApiJobDirectory as ApiJobDirectoryV27_0_0, type ApiKeyCreate, type ApiVersion, type ApiVersionResponse, AppState, type ApplianceProtocol, type ArgsOf, AuthError, AuthErrorCode, type AuthResponse, type BaseApiDirectory, type CallMethod, type CallParams, type CallResponse, type Container, type CreateClientOptions, type DefaultApiDirectory, type EventKind, type EventName, type EventUnion, InvalidVersionResponseError, type Job, type JobMethod, type JobParams, type JobProgress, type JobResult, JobState, type Logger, NoCompatibleVersionsError, type OperationMappings, type QueryDirectory, type QueryEntity, type QueryListOptions, type QueryMethod, type QuerySingleOptions, SUPPORTED_API_VERSIONS, type SmbStatusInfoLevel, type SmbStatusOptions, type SmbStatusRequest, type SmbStatusResponse, type SmbStatusRow, type SupportedApiVersion, TrueNasApiClient, TrueNasApiClientV2510, TrueNasApiClientV26, TrueNasApiClientV27, TrueNasAuthMechanism, type TrueNasDate, UserRole, type UserRoleName, VersionCompatibility, VersionDiscovery, VersionDiscoveryError, VersionDiscoveryNetworkError, VersionDiscoveryTimeoutError, VersionEndpointNotFoundError, VersionTooNewError, VersionTooOldError, consoleLogger, createTrueNasClient, getApiErrorMessage, isJobFinished, noopLogger, index$7 as v25_10_0, index$6 as v25_10_1, index$5 as v25_10_2, index$4 as v25_10_3, index$3 as v25_10_4, index$2 as v25_10_5, index$1 as v26_0_0, index as v27_0_0 };
|
package/dist/index.js
CHANGED
|
@@ -2387,8 +2387,11 @@ var apiVersionConfig = {
|
|
|
2387
2387
|
* - DNS lookup failure
|
|
2388
2388
|
* - Server unreachable
|
|
2389
2389
|
*
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2390
|
+
* Which is why it is no longer reached on all of them. A discovery failure is
|
|
2391
|
+
* now put to a reachability probe and a second attempt first, and only an
|
|
2392
|
+
* appliance that answers the probe and still will not serve `/api/versions`
|
|
2393
|
+
* is assumed to be this version. One that answers nothing is reported as
|
|
2394
|
+
* unreachable rather than pinned to a version nobody confirmed.
|
|
2392
2395
|
*/
|
|
2393
2396
|
FALLBACK_VERSION: "v25.10.0"
|
|
2394
2397
|
};
|
|
@@ -3444,6 +3447,16 @@ function toAppState(state) {
|
|
|
3444
3447
|
}
|
|
3445
3448
|
}
|
|
3446
3449
|
|
|
3450
|
+
// src/utils/smb-status.utils.ts
|
|
3451
|
+
function toSmbStatusParams(request = {}) {
|
|
3452
|
+
return [
|
|
3453
|
+
request.infoLevel ?? "ALL",
|
|
3454
|
+
request.filters ?? [],
|
|
3455
|
+
request.options ?? {},
|
|
3456
|
+
request.statusOptions ?? {}
|
|
3457
|
+
];
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3447
3460
|
// src/client/truenas-api-client-v25-10.ts
|
|
3448
3461
|
var TrueNasApiClientV2510 = class extends TrueNasApiClient {
|
|
3449
3462
|
/**
|
|
@@ -3483,6 +3496,42 @@ var TrueNasApiClientV2510 = class extends TrueNasApiClient {
|
|
|
3483
3496
|
);
|
|
3484
3497
|
}
|
|
3485
3498
|
return this.api.job("virt.instance.delete", [id]);
|
|
3499
|
+
},
|
|
3500
|
+
// The private leg. See `PrivateSmbStatusCall` for why this is a cast and
|
|
3501
|
+
// not a directory lookup.
|
|
3502
|
+
//
|
|
3503
|
+
// v25.10 answers rather than refusing, for a caller with the authority.
|
|
3504
|
+
// Being private does not stop the method running: `ws_handler/rpc.py`
|
|
3505
|
+
// logs a warning for a private call over websocket and dispatches it
|
|
3506
|
+
// anyway — the `# FIXME: Eventually, prohibit this` above that branch is
|
|
3507
|
+
// still unresolved on master.
|
|
3508
|
+
//
|
|
3509
|
+
// It does decide who may run it. `smb.status` carries no `roles=` here,
|
|
3510
|
+
// and middleware role-registers a method only `if roles:`, so it is in no
|
|
3511
|
+
// role's allowlist and only the non-STIG full-admin wildcard reaches it.
|
|
3512
|
+
// That is documented on `OperationMappings.smbStatus`; this leg does not
|
|
3513
|
+
// pre-empt it. A caller without the authority gets middleware's `EACCES`,
|
|
3514
|
+
// which is the honest answer and the server's to give.
|
|
3515
|
+
//
|
|
3516
|
+
// The server-side warning is per dispatch, and a client count is exactly
|
|
3517
|
+
// the sort of thing a caller polls. Reported at debug rather than warn,
|
|
3518
|
+
// because nothing here is degraded or unhonoured — the request is served
|
|
3519
|
+
// in full — and left to the caller to weigh.
|
|
3520
|
+
//
|
|
3521
|
+
// Future tense, and for the same reason as `containerDelete` above: this
|
|
3522
|
+
// runs when the operation is built, and `api.call` is cold, so no request
|
|
3523
|
+
// has gone out yet and may never. One built observable subscribed twice
|
|
3524
|
+
// dispatches twice; this line is what will happen, not a record that it
|
|
3525
|
+
// did.
|
|
3526
|
+
smbStatus: (request) => {
|
|
3527
|
+
this.logger.debug(
|
|
3528
|
+
"smbStatus: smb.status is private on v25.10; each dispatch will log a warning server-side and needs a non-STIG full-admin session",
|
|
3529
|
+
{ method: "smb.status", infoLevel: request?.infoLevel ?? "ALL" }
|
|
3530
|
+
);
|
|
3531
|
+
return this.api.call(
|
|
3532
|
+
"smb.status",
|
|
3533
|
+
toSmbStatusParams(request)
|
|
3534
|
+
);
|
|
3486
3535
|
}
|
|
3487
3536
|
};
|
|
3488
3537
|
}
|
|
@@ -3570,7 +3619,16 @@ var TrueNasApiClientV26 = class extends TrueNasApiClient {
|
|
|
3570
3619
|
containerDelete: (id, options) => this.api.job(
|
|
3571
3620
|
"container.delete",
|
|
3572
3621
|
options ? [parseInt(id, 10), options] : [parseInt(id, 10)]
|
|
3573
|
-
)
|
|
3622
|
+
),
|
|
3623
|
+
// `smb.status` is public here and gated on `SHARING_SMB_READ`, so it is
|
|
3624
|
+
// an ordinary `call` read straight out of the generated directory — the
|
|
3625
|
+
// one leg of this operation that needs no assertion about the server.
|
|
3626
|
+
//
|
|
3627
|
+
// Not a query verb: middleware returns the same `list | dict | int`
|
|
3628
|
+
// polymorphism a `.query` does, but the generator did not mark the entry
|
|
3629
|
+
// with an `entity`, so `api.query` does not accept it and the union is
|
|
3630
|
+
// handed to the caller to narrow.
|
|
3631
|
+
smbStatus: (request) => this.api.call("smb.status", toSmbStatusParams(request))
|
|
3574
3632
|
};
|
|
3575
3633
|
}
|
|
3576
3634
|
};
|
|
@@ -3651,7 +3709,16 @@ var TrueNasApiClientV27 = class extends TrueNasApiClient {
|
|
|
3651
3709
|
containerDelete: (id, options) => this.api.job(
|
|
3652
3710
|
"container.delete",
|
|
3653
3711
|
options ? [parseInt(id, 10), options] : [parseInt(id, 10)]
|
|
3654
|
-
)
|
|
3712
|
+
),
|
|
3713
|
+
// `smb.status` is public here and gated on `SHARING_SMB_READ`, so it is
|
|
3714
|
+
// an ordinary `call` read straight out of the generated directory — the
|
|
3715
|
+
// one leg of this operation that needs no assertion about the server.
|
|
3716
|
+
//
|
|
3717
|
+
// Not a query verb: middleware returns the same `list | dict | int`
|
|
3718
|
+
// polymorphism a `.query` does, but the generator did not mark the entry
|
|
3719
|
+
// with an `entity`, so `api.query` does not accept it and the union is
|
|
3720
|
+
// handed to the caller to narrow.
|
|
3721
|
+
smbStatus: (request) => this.api.call("smb.status", toSmbStatusParams(request))
|
|
3655
3722
|
};
|
|
3656
3723
|
}
|
|
3657
3724
|
};
|
|
@@ -3751,6 +3818,12 @@ var InvalidVersionResponseError = class extends VersionDiscoveryError {
|
|
|
3751
3818
|
}
|
|
3752
3819
|
};
|
|
3753
3820
|
var discoveryTimeoutMs = 5e3;
|
|
3821
|
+
var probeTimeoutMs = 5e3;
|
|
3822
|
+
function corsIsEnforced() {
|
|
3823
|
+
const hasDocument = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
3824
|
+
const inWorker = typeof globalThis.WorkerGlobalScope !== "undefined";
|
|
3825
|
+
return hasDocument || inWorker;
|
|
3826
|
+
}
|
|
3754
3827
|
function hasErrorName(error, expected) {
|
|
3755
3828
|
return typeof error === "object" && error !== null && "name" in error && error.name === expected;
|
|
3756
3829
|
}
|
|
@@ -3763,6 +3836,42 @@ var VersionDiscovery = class {
|
|
|
3763
3836
|
versionsUrl(hostname) {
|
|
3764
3837
|
return `${httpScheme(this.protocol)}//${hostname}/api/versions`;
|
|
3765
3838
|
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Whether the appliance answers at all, asked in a way CORS cannot block.
|
|
3841
|
+
*
|
|
3842
|
+
* A `no-cors` request yields a response the page may not read, but its
|
|
3843
|
+
* *arrival* is the whole answer here: something served it. That separates the
|
|
3844
|
+
* two failures `fetch` reports identically — a box that refused to share its
|
|
3845
|
+
* versions with this origin, and a box that is not there.
|
|
3846
|
+
*
|
|
3847
|
+
* `cannot-ask` where CORS is not enforced: nothing is blocking anything, so a
|
|
3848
|
+
* failed fetch already means unreachable and a probe would only repeat what
|
|
3849
|
+
* discovery just found. It is not a quieter way of saying "no".
|
|
3850
|
+
*/
|
|
3851
|
+
async probeReachable(hostname) {
|
|
3852
|
+
if (!corsIsEnforced()) return "cannot-ask";
|
|
3853
|
+
const url = this.versionsUrl(hostname);
|
|
3854
|
+
const controller = new AbortController();
|
|
3855
|
+
const timer2 = setTimeout(() => controller.abort(), probeTimeoutMs);
|
|
3856
|
+
try {
|
|
3857
|
+
await fetch(url, {
|
|
3858
|
+
mode: "no-cors",
|
|
3859
|
+
cache: "no-store",
|
|
3860
|
+
signal: controller.signal
|
|
3861
|
+
});
|
|
3862
|
+
this.logger.info("Reachability probe answered", { hostname, url });
|
|
3863
|
+
return "reachable";
|
|
3864
|
+
} catch (error) {
|
|
3865
|
+
this.logger.warn("Reachability probe got no answer", {
|
|
3866
|
+
hostname,
|
|
3867
|
+
url,
|
|
3868
|
+
error
|
|
3869
|
+
});
|
|
3870
|
+
return "silent";
|
|
3871
|
+
} finally {
|
|
3872
|
+
clearTimeout(timer2);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3766
3875
|
/**
|
|
3767
3876
|
* Discovers the API version for a given hostname.
|
|
3768
3877
|
*
|
|
@@ -4022,30 +4131,81 @@ async function createTrueNasClient(opts) {
|
|
|
4022
4131
|
});
|
|
4023
4132
|
throw error;
|
|
4024
4133
|
}
|
|
4025
|
-
const
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
logger.error("Invalid fallback version configuration", {
|
|
4029
|
-
uuid: uuid.slice(0, 8),
|
|
4030
|
-
hostnames: hostnames.join(", "),
|
|
4031
|
-
fallbackVersion: fallbackVersionString
|
|
4032
|
-
});
|
|
4033
|
-
throw error;
|
|
4134
|
+
const reachable = await probeAnyHostname(hostnames, versionDiscovery);
|
|
4135
|
+
if (reachable === "silent") {
|
|
4136
|
+
await sleep(unreachableRetryDelayMs);
|
|
4034
4137
|
}
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4138
|
+
try {
|
|
4139
|
+
const retryWinner = await discoverVersionFromAnyHostname(
|
|
4140
|
+
hostnames,
|
|
4141
|
+
versionDiscovery
|
|
4142
|
+
);
|
|
4143
|
+
logger.info("Version discovery succeeded on retry", {
|
|
4038
4144
|
uuid: uuid.slice(0, 8),
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4145
|
+
hostname: retryWinner.hostname,
|
|
4146
|
+
version: retryWinner.version.version,
|
|
4147
|
+
firstError: errorMessage
|
|
4148
|
+
});
|
|
4149
|
+
version = retryWinner.version;
|
|
4150
|
+
} catch (retryError) {
|
|
4151
|
+
if (!(retryError instanceof VersionDiscoveryNetworkError)) {
|
|
4152
|
+
throw retryError;
|
|
4043
4153
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4154
|
+
if (reachable !== "reachable") {
|
|
4155
|
+
logger.error(
|
|
4156
|
+
"Version discovery failed and the appliance did not answer a reachability probe; not assuming a version",
|
|
4157
|
+
{
|
|
4158
|
+
uuid: uuid.slice(0, 8),
|
|
4159
|
+
hostnames: hostnames.join(", "),
|
|
4160
|
+
probe: reachable === "silent" ? "no answer" : "not applicable",
|
|
4161
|
+
error: errorMessageOrDefault(retryError, "Unknown error")
|
|
4162
|
+
}
|
|
4163
|
+
);
|
|
4164
|
+
throw retryError;
|
|
4165
|
+
}
|
|
4166
|
+
const fallbackVersionString = apiVersionConfig.FALLBACK_VERSION;
|
|
4167
|
+
const fallbackVersion = parseApiVersion(fallbackVersionString);
|
|
4168
|
+
if (!fallbackVersion) {
|
|
4169
|
+
logger.error("Invalid fallback version configuration", {
|
|
4170
|
+
uuid: uuid.slice(0, 8),
|
|
4171
|
+
hostnames: hostnames.join(", "),
|
|
4172
|
+
fallbackVersion: fallbackVersionString
|
|
4173
|
+
});
|
|
4174
|
+
throw retryError;
|
|
4175
|
+
}
|
|
4176
|
+
logger.warn(
|
|
4177
|
+
"Appliance is reachable but version discovery is still blocked; falling back to assumed version",
|
|
4178
|
+
{
|
|
4179
|
+
uuid: uuid.slice(0, 8),
|
|
4180
|
+
hostnames: hostnames.join(", "),
|
|
4181
|
+
fallbackVersion: fallbackVersionString,
|
|
4182
|
+
originalError: errorMessage,
|
|
4183
|
+
warning: "The appliance answered a probe but not /api/versions, which is what v25.10.0 looks like from a browser. If it is newer than that, this client is now pinned to the wrong surface."
|
|
4184
|
+
}
|
|
4185
|
+
);
|
|
4186
|
+
version = fallbackVersion;
|
|
4187
|
+
}
|
|
4046
4188
|
}
|
|
4047
4189
|
return instantiateClientForVersion(version, opts, logger);
|
|
4048
4190
|
}
|
|
4191
|
+
var unreachableRetryDelayMs = 2500;
|
|
4192
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4193
|
+
async function probeAnyHostname(hostnames, versionDiscovery) {
|
|
4194
|
+
const probes = hostnames.map(
|
|
4195
|
+
(hostname) => versionDiscovery.probeReachable(hostname)
|
|
4196
|
+
);
|
|
4197
|
+
const remaining = new Set(probes);
|
|
4198
|
+
let sawSilence = false;
|
|
4199
|
+
while (remaining.size > 0) {
|
|
4200
|
+
const settled = await Promise.race(
|
|
4201
|
+
[...remaining].map((probe) => probe.then((result) => ({ probe, result })))
|
|
4202
|
+
);
|
|
4203
|
+
if (settled.result === "reachable") return "reachable";
|
|
4204
|
+
if (settled.result === "silent") sawSilence = true;
|
|
4205
|
+
remaining.delete(settled.probe);
|
|
4206
|
+
}
|
|
4207
|
+
return sawSilence ? "silent" : "cannot-ask";
|
|
4208
|
+
}
|
|
4049
4209
|
async function discoverVersionFromAnyHostname(hostnames, versionDiscovery) {
|
|
4050
4210
|
const attempts = hostnames.map(
|
|
4051
4211
|
(hostname) => firstValueFrom(versionDiscovery.discoverVersion(hostname)).then(
|