@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/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`, `fetch` rejects, and that is indistinguishable from the CORS block
30433
- * v25.10.0 has on `/api/versions` — so the fallback fires and the caller gets
30434
- * a client pinned to v25.10.0, warned about only in the log.
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
- * Note that when the selected failure is a `VersionDiscoveryNetworkError`,
30549
- * this function attempts to use a fallback API version (see `FALLBACK_VERSION`)
30550
- * because network errors are actually expected on 25.10.0 systems due to a CORS
30551
- * bug. A network error alongside a version-compatibility error or a 404 does
30552
- * *not* reach the fallback — see `selectRepresentativeFailure`.
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
- * Those four are currently identical to v26's, because v27 inherits all three
30673
- * container entries the facade touches rather than re-declaring them. Asserted
30674
- * rather than assumed, and re-runnable: the spec pins all three against v26's,
30675
- * so the day they diverge is a failure there.
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 the guard against them drifting
30684
- * silently: an entry that changes at v27 stops compiling here.
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} — the sentinel the client factory keys on for
30712
- * its CORS fallback (replacing the old `HttpErrorResponse.status === 0` check).
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
- * Using this fallback means systems with genuine network issues will still
2391
- * attempt connection (and fail during WebSocket handshake instead of immediately).
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 fallbackVersionString = apiVersionConfig.FALLBACK_VERSION;
4026
- const fallbackVersion = parseApiVersion(fallbackVersionString);
4027
- if (!fallbackVersion) {
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
- logger.warn(
4036
- "Version discovery failed with a network error (possible CORS or network issue), falling back to assumed version",
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
- hostnames: hostnames.join(", "),
4040
- fallbackVersion: fallbackVersionString,
4041
- originalError: errorMessage,
4042
- warning: "A network error has multiple causes (CORS, network down, DNS failure). The connection may still fail during the WebSocket handshake."
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
- version = fallbackVersion;
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(