@ceralive/modem-control 1.1.0 → 1.2.0

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.
Files changed (56) hide show
  1. package/README.md +30 -6
  2. package/dist/backend/at-lease.d.ts +3 -1
  3. package/dist/backend/at-lease.js +11 -2
  4. package/dist/backend/recovery-ladder.d.ts +1 -1
  5. package/dist/backend/transition-preconditions.d.ts +35 -10
  6. package/dist/backend/transition-preconditions.js +78 -10
  7. package/dist/backend/usb-mode-transition.js +39 -19
  8. package/dist/capability/five-g-preference.d.ts +2 -2
  9. package/dist/capability/support-claim.d.ts +2 -2
  10. package/dist/domain/shadow-divergence.d.ts +1 -1
  11. package/dist/fcc/coverage.d.ts +20 -20
  12. package/dist/hardware/router-parsers.d.ts +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/journal/legacy-ceraui.d.ts +2 -2
  16. package/dist/observations/metric.d.ts +15 -1
  17. package/dist/observations/provenance.d.ts +2 -2
  18. package/dist/observations/reading.d.ts +1 -1
  19. package/dist/observations/state-separation.d.ts +1 -1
  20. package/dist/operation-ids.d.ts +2 -0
  21. package/dist/operation-ids.js +26 -0
  22. package/dist/ports/location.d.ts +1 -1
  23. package/dist/providers/contracts.d.ts +2 -2
  24. package/dist/providers/huawei-hilink/provider.d.ts +12 -12
  25. package/dist/providers/huawei-hilink/runtime.d.ts +2 -2
  26. package/dist/providers/modem-manager/errors.d.ts +1 -1
  27. package/dist/providers/modem-manager/generic-operations.d.ts +3 -1
  28. package/dist/providers/modem-manager/generic-operations.js +3 -1
  29. package/dist/providers/modem-manager/index.d.ts +1 -0
  30. package/dist/providers/modem-manager/index.js +1 -0
  31. package/dist/providers/modem-manager/provider.d.ts +2 -0
  32. package/dist/providers/modem-manager/provider.js +3 -0
  33. package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
  34. package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
  35. package/dist/providers/modem-manager/types.d.ts +2 -0
  36. package/dist/providers/network-manager/types.d.ts +11 -3
  37. package/dist/providers/ufi-himi/operations.d.ts +1 -1
  38. package/dist/providers/ufi-himi/prohibitions.d.ts +22 -22
  39. package/dist/providers/ufi-himi/provider.d.ts +4 -4
  40. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +1 -1
  41. package/dist/providers/ufi-himi/transport.d.ts +2 -2
  42. package/dist/providers/zte-goform/provider.d.ts +18 -13
  43. package/dist/providers/zte-goform/provider.js +16 -16
  44. package/dist/providers/zte-goform/session.d.ts +8 -2
  45. package/dist/providers/zte-goform/session.js +95 -31
  46. package/dist/radio/mode-combinations.d.ts +11 -1
  47. package/dist/safety/flock-resource-ownership.js +18 -31
  48. package/dist/usb-mode/catalog-schema.d.ts +18 -18
  49. package/dist/usb-mode/index.d.ts +1 -0
  50. package/dist/usb-mode/index.js +1 -0
  51. package/dist/usb-mode/ingestion.d.ts +4 -4
  52. package/dist/usb-mode/runtime-capability.d.ts +59 -0
  53. package/dist/usb-mode/runtime-capability.js +157 -0
  54. package/dist/ussd/refusal.d.ts +1 -1
  55. package/dist/ussd/session.d.ts +24 -2
  56. package/package.json +1 -1
@@ -4,27 +4,27 @@ export const ZTE_PATHS = {
4
4
  get: '/goform/goform_get_cmd_process',
5
5
  set: '/goform/goform_set_cmd_process',
6
6
  };
7
+ export const ZTE_EVIDENCE_CMD = 'LD,psw_fail_num_str,login_lock_time,wa_inner_version,cr_version';
7
8
  export const ZTE_PROFILES = [
8
9
  { id: 'mf79u-legacy', firmware: 'MF79U', algorithm: 'legacy-base64' },
9
- { id: 'mf266-salted', firmware: 'MF266', algorithm: 'salted-sha256' },
10
+ { id: 'mf79u-ld-salted', firmware: 'MF79U', algorithm: 'login-salted-sha256' },
11
+ { id: 'mf266-salted', firmware: 'MF266', algorithm: 'multi-user-salted-sha256' },
10
12
  ];
11
13
  export const ZTE_UNKNOWN_PROFILE = 'zte-unknown-read-only';
12
- export function zteProfileForFirmware(firmware) {
13
- return ZTE_PROFILES.find((profile) => profile.firmware === firmware);
14
+ export function zteProfilesForFirmware(firmware) {
15
+ return ZTE_PROFILES.filter((profile) => profile.firmware === firmware);
14
16
  }
15
17
  export function zteProfileById(profileId) {
16
18
  return ZTE_PROFILES.find((profile) => profile.id === profileId);
17
19
  }
18
20
  export class ZteGoformProvider extends ZteSessionRuntime {
19
21
  async fingerprint(request) {
20
- const response = await this.get('cr_version,network_type');
21
- const record = response.status === 200 ? parseZteRecord(response.body) : undefined;
22
- const profile = zteProfileForFirmware(request.firmware);
23
- const matches = record !== undefined;
22
+ const evidence = await this.probeEvidence(request);
23
+ const matches = evidence !== undefined;
24
24
  return {
25
25
  signal: matches ? 'match' : 'unknown',
26
26
  strength: 'strong',
27
- profiles: matches ? [profile?.id ?? ZTE_UNKNOWN_PROFILE] : [],
27
+ profiles: matches ? [evidence.profile?.id ?? ZTE_UNKNOWN_PROFILE] : [],
28
28
  detail: matches ? 'zte-goform-shape' : 'zte-goform-not-proven',
29
29
  };
30
30
  }
@@ -63,13 +63,13 @@ export function createZteGoformDefinition(options) {
63
63
  const runtime = new ZteGoformProvider(options);
64
64
  return {
65
65
  id: 'zte-goform',
66
- profileVersion: '1',
66
+ profileVersion: '2',
67
67
  eligibleTransports: ['network'],
68
- passiveMatchers: ZTE_PROFILES.map((profile) => ({
69
- id: `firmware-${profile.firmware}`,
68
+ passiveMatchers: ['MF79U', 'MF266'].map((firmware) => ({
69
+ id: `firmware-${firmware}`,
70
70
  fact: 'firmware',
71
- expected: [profile.firmware],
72
- profiles: [profile.id],
71
+ expected: [firmware],
72
+ profiles: zteProfilesForFirmware(firmware).map((profile) => profile.id),
73
73
  strength: 'strong',
74
74
  required: true,
75
75
  })),
@@ -77,7 +77,7 @@ export function createZteGoformDefinition(options) {
77
77
  { id: 'zte-goform-shape', run: (request) => runtime.fingerprint(request) },
78
78
  ],
79
79
  authenticatedProfile: {
80
- algorithm: 'firmware-selected-zte-goform',
80
+ algorithm: 'evidence-selected-zte-goform',
81
81
  attemptLimit: 1,
82
82
  authenticate: (request, candidates) => runtime.authenticateProfile(request, candidates),
83
83
  },
@@ -89,11 +89,11 @@ export function createZteGoformDefinition(options) {
89
89
  request: {
90
90
  method: 'POST',
91
91
  path: ZTE_PATHS.set,
92
- goformId: profile.algorithm === 'legacy-base64' ? 'LOGIN' : 'LOGIN_MULTI_USER',
92
+ goformId: profile.algorithm === 'multi-user-salted-sha256' ? 'LOGIN_MULTI_USER' : 'LOGIN',
93
93
  interfaceBound: true,
94
94
  redirects: 'disabled',
95
95
  },
96
- response: profile.algorithm === 'salted-sha256'
96
+ response: profile.algorithm === 'multi-user-salted-sha256'
97
97
  ? { status: 'matched', sessionMaterial: '[redacted]', ad: '[redacted]' }
98
98
  : { status: 'matched', sessionMaterial: '[redacted]' },
99
99
  })),
@@ -1,17 +1,23 @@
1
1
  import type { AuthenticatedProfileResult, ProviderMatchRequest } from '../contracts.js';
2
- import { type ZteOptions } from './provider.js';
2
+ import { type ZteOptions, type ZteProfile } from './provider.js';
3
3
  import type { ZteHttpResponse } from './transport.js';
4
+ type ZteEvidence = {
5
+ readonly record: Readonly<Record<string, string | number>>;
6
+ readonly profile: ZteProfile | undefined;
7
+ };
4
8
  export declare function parseZteRecord(body: string): Readonly<Record<string, string | number>> | undefined;
5
9
  export declare class ZteSessionRuntime {
6
10
  #private;
7
11
  protected readonly options: ZteOptions;
8
12
  constructor(options: ZteOptions);
9
13
  authenticateProfile(request: ProviderMatchRequest, candidates: readonly string[]): Promise<AuthenticatedProfileResult>;
14
+ protected probeEvidence(request: ProviderMatchRequest): Promise<ZteEvidence | undefined>;
10
15
  protected sessionCookie(request: ProviderMatchRequest): string | undefined;
11
16
  protected get(cmd: string, cookie?: string, multiData?: boolean): Promise<ZteHttpResponse>;
12
- private loginLegacy;
17
+ private login;
13
18
  private loginSalted;
14
19
  private sessionKey;
15
20
  private post;
16
21
  private request;
17
22
  }
23
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { z } from 'zod';
3
- import { ZTE_PATHS, ZTE_UNKNOWN_PROFILE, zteProfileById, zteProfileForFirmware, } from './provider.js';
3
+ import { ZTE_EVIDENCE_CMD, ZTE_PATHS, ZTE_UNKNOWN_PROFILE, zteProfileById, zteProfilesForFirmware, } from './provider.js';
4
4
  const flatRecordSchema = z.record(z.string(), z.union([z.string(), z.number()]));
5
5
  export function parseZteRecord(body) {
6
6
  const result = z
@@ -29,9 +29,47 @@ function stokCookie(response) {
29
29
  function sha256(value) {
30
30
  return createHash('sha256').update(value).digest('hex').toUpperCase();
31
31
  }
32
+ function evidenceNumber(value) {
33
+ if (typeof value === 'string' && value.trim() === '')
34
+ return undefined;
35
+ const parsed = typeof value === 'number' ? value : Number(value);
36
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
37
+ }
38
+ function profileFromEvidence(firmware, record) {
39
+ const versionEvidence = `${record.wa_inner_version ?? ''} ${record.cr_version ?? ''}`.toUpperCase();
40
+ const family = versionEvidence.includes('MF266')
41
+ ? 'MF266'
42
+ : versionEvidence.includes('MF79U')
43
+ ? 'MF79U'
44
+ : firmware;
45
+ const hasLd = typeof record.LD === 'string' && record.LD.length > 0;
46
+ const profileId = family === 'MF266'
47
+ ? hasLd
48
+ ? 'mf266-salted'
49
+ : undefined
50
+ : family === 'MF79U'
51
+ ? hasLd
52
+ ? 'mf79u-ld-salted'
53
+ : 'mf79u-legacy'
54
+ : undefined;
55
+ return zteProfilesForFirmware(family).find((profile) => profile.id === profileId);
56
+ }
57
+ function classifyLogin(response) {
58
+ const result = parseZteRecord(response.body)?.result;
59
+ if (response.status !== 200 || result === '1') {
60
+ return { status: 'refused', detail: 'protocol-mismatch' };
61
+ }
62
+ if (result === '3')
63
+ return { status: 'refused', detail: 'auth-rejection' };
64
+ const cookie = stokCookie(response);
65
+ return result === '0' && cookie !== undefined
66
+ ? { status: 'accepted', cookie }
67
+ : { status: 'refused', detail: 'protocol-mismatch' };
68
+ }
32
69
  export class ZteSessionRuntime {
33
70
  options;
34
71
  #sessions = new Map();
72
+ #evidence = new Map();
35
73
  constructor(options) {
36
74
  this.options = options;
37
75
  }
@@ -41,12 +79,48 @@ export class ZteSessionRuntime {
41
79
  return { status: 'matched', profile: ZTE_UNKNOWN_PROFILE, detail: 'read-only-fingerprint' };
42
80
  }
43
81
  const profile = zteProfileById(candidate ?? '');
44
- if (profile === undefined || zteProfileForFirmware(request.firmware)?.id !== profile.id) {
82
+ if (profile === undefined) {
45
83
  return { status: 'refused', detail: 'profile-mismatch' };
46
84
  }
47
- return profile.algorithm === 'legacy-base64'
48
- ? this.loginLegacy(request, profile)
49
- : this.loginSalted(request, profile);
85
+ const evidence = this.#evidence.get(this.sessionKey(request)) ?? (await this.probeEvidence(request));
86
+ if (evidence === undefined)
87
+ return { status: 'refused', detail: 'protocol-mismatch' };
88
+ const remainingAttempts = evidenceNumber(evidence.record.psw_fail_num_str);
89
+ const lockTime = evidenceNumber(evidence.record.login_lock_time);
90
+ if (remainingAttempts === undefined || lockTime === undefined) {
91
+ return { status: 'refused', detail: 'protocol-mismatch' };
92
+ }
93
+ if (lockTime > 0 || remainingAttempts === 0) {
94
+ return { status: 'refused', detail: 'lockout' };
95
+ }
96
+ if (evidence.profile?.id !== profile.id) {
97
+ return { status: 'refused', detail: 'protocol-mismatch' };
98
+ }
99
+ switch (profile.algorithm) {
100
+ case 'legacy-base64':
101
+ return this.login(request, profile, Buffer.from(this.options.credentials.password).toString('base64'));
102
+ case 'login-salted-sha256': {
103
+ const ld = evidence.record.LD;
104
+ if (typeof ld !== 'string')
105
+ return { status: 'refused', detail: 'protocol-mismatch' };
106
+ return this.login(request, profile, sha256(`${sha256(this.options.credentials.password)}${ld}`));
107
+ }
108
+ case 'multi-user-salted-sha256':
109
+ return this.loginSalted(request, profile, evidence.record);
110
+ default: {
111
+ const exhaustive = profile.algorithm;
112
+ return exhaustive;
113
+ }
114
+ }
115
+ }
116
+ async probeEvidence(request) {
117
+ const response = await this.get(ZTE_EVIDENCE_CMD, undefined, true);
118
+ const record = response.status === 200 ? parseZteRecord(response.body) : undefined;
119
+ if (record === undefined)
120
+ return undefined;
121
+ const evidence = { record, profile: profileFromEvidence(request.firmware, record) };
122
+ this.#evidence.set(this.sessionKey(request), evidence);
123
+ return evidence;
50
124
  }
51
125
  sessionCookie(request) {
52
126
  return this.#sessions.get(this.sessionKey(request))?.cookie;
@@ -57,27 +131,21 @@ export class ZteSessionRuntime {
57
131
  query.set('multi_data', '1');
58
132
  return this.request('GET', `${ZTE_PATHS.get}?${query.toString()}`, undefined, cookie);
59
133
  }
60
- async loginLegacy(request, profile) {
134
+ async login(request, profile, password) {
61
135
  const response = await this.post(new URLSearchParams({
62
136
  goformId: 'LOGIN',
63
137
  isTest: 'false',
64
- password: Buffer.from(this.options.credentials.password).toString('base64'),
138
+ password,
65
139
  }).toString());
66
- const record = parseZteRecord(response.body);
67
- const cookie = stokCookie(response);
68
- if (response.status !== 200 || record?.result !== '0' || cookie === undefined) {
69
- return {
70
- status: 'refused',
71
- detail: record?.LD === undefined ? 'auth-rejection' : 'protocol-mismatch',
72
- };
73
- }
74
- this.#sessions.set(this.sessionKey(request), { cookie });
140
+ const result = classifyLogin(response);
141
+ if (result.status === 'refused')
142
+ return result;
143
+ this.#sessions.set(this.sessionKey(request), { cookie: result.cookie });
75
144
  return { status: 'matched', profile: profile.id, detail: 'login-ok' };
76
145
  }
77
- async loginSalted(request, profile) {
78
- const ldResponse = await this.get('LD');
79
- const ld = parseZteRecord(ldResponse.body)?.LD;
80
- if (ldResponse.status !== 200 || typeof ld !== 'string') {
146
+ async loginSalted(request, profile, evidence) {
147
+ const ld = evidence.LD;
148
+ if (typeof ld !== 'string') {
81
149
  return { status: 'refused', detail: 'protocol-mismatch' };
82
150
  }
83
151
  const login = await this.post(new URLSearchParams({
@@ -87,21 +155,17 @@ export class ZteSessionRuntime {
87
155
  IP: 'localhost',
88
156
  user: this.options.credentials.username,
89
157
  }).toString());
90
- const cookie = stokCookie(login);
91
- if (login.status !== 200 ||
92
- parseZteRecord(login.body)?.result !== '0' ||
93
- cookie === undefined) {
94
- return { status: 'refused', detail: 'auth-rejection' };
95
- }
96
- const versions = parseZteRecord((await this.get('wa_inner_version,cr_version', cookie, true)).body);
97
- const rd = parseZteRecord((await this.get('RD', cookie)).body)?.RD;
98
- const waVersion = versions?.wa_inner_version;
99
- const crVersion = versions?.cr_version;
158
+ const result = classifyLogin(login);
159
+ if (result.status === 'refused')
160
+ return result;
161
+ const rd = parseZteRecord((await this.get('RD', result.cookie)).body)?.RD;
162
+ const waVersion = evidence.wa_inner_version;
163
+ const crVersion = evidence.cr_version;
100
164
  if (typeof waVersion !== 'string' || typeof crVersion !== 'string' || typeof rd !== 'string') {
101
165
  return { status: 'refused', detail: 'protocol-mismatch' };
102
166
  }
103
167
  this.#sessions.set(this.sessionKey(request), {
104
- cookie,
168
+ cookie: result.cookie,
105
169
  ad: sha256(`${sha256(`${waVersion}${crVersion}`)}${rd}`),
106
170
  });
107
171
  return { status: 'matched', profile: profile.id, detail: 'login-ok' };
@@ -28,7 +28,17 @@ export declare function encodeModeNames(names: readonly ModeName[]): {
28
28
  readonly unknown: ModeName;
29
29
  };
30
30
  /** Why a combination could not be fully placed. Never a reason to hide it. */
31
- export declare const MODE_COMBINATION_ANOMALIES: readonly ["unnamed-allowed-bit", "unnamed-preferred-bit", "preferred-not-in-allowed", "preferred-not-singular", "empty-allowed"];
31
+ export declare const MODE_COMBINATION_ANOMALIES: readonly [
32
+ /** The allowed mask carries a bit this build does not name. */
33
+ 'unnamed-allowed-bit',
34
+ /** The preferred mask carries a bit this build does not name. */
35
+ 'unnamed-preferred-bit',
36
+ /** The preferred mode is not a member of the allowed set. */
37
+ 'preferred-not-in-allowed',
38
+ /** The preferred mask names more than one mode; MM's contract is at most one. */
39
+ 'preferred-not-singular',
40
+ /** The allowed mask is zero — nothing can be selected from this combination. */
41
+ 'empty-allowed'];
32
42
  export type ModeCombinationAnomaly = (typeof MODE_COMBINATION_ANOMALIES)[number];
33
43
  export type ModeCombinationClassification = 'named' | 'unknown-combination';
34
44
  /**
@@ -1,14 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFile } from 'node:fs/promises';
3
- const LOCK_HELPER = String.raw `
4
- import { writeFileSync } from 'node:fs';
5
- const lockPath = process.env.CERALIVE_MODEM_CONTROL_LOCK_PATH;
6
- if (!lockPath) process.exit(64);
7
- const holder = { pid: process.pid, startedAtEpochMs: Date.now() };
8
- writeFileSync(lockPath, JSON.stringify(holder) + '\n', { mode: 0o600 });
9
- process.stdout.write(JSON.stringify({ type: 'acquired', holder }) + '\n');
10
- process.stdin.resume();
11
- `;
2
+ import { readFile, writeFile } from 'node:fs/promises';
12
3
  export class FlockResourceOwnershipError extends Error {
13
4
  detail;
14
5
  name = 'FlockResourceOwnershipError';
@@ -20,23 +11,13 @@ export class FlockResourceOwnershipError extends Error {
20
11
  export function createFlockResourceOwnershipPort(options) {
21
12
  return {
22
13
  async acquire() {
23
- const child = spawn(options.flockBinary ?? 'flock', [
24
- '--exclusive',
25
- '--nonblock',
26
- '--no-fork',
27
- options.lockPath,
28
- process.execPath,
29
- '-e',
30
- LOCK_HELPER,
31
- ], {
32
- env: {
33
- ...process.env,
34
- CERALIVE_MODEM_CONTROL_LOCK_PATH: options.lockPath,
35
- },
36
- stdio: ['pipe', 'pipe', 'pipe'],
37
- });
14
+ const child = spawn(options.flockBinary ?? 'flock', ['--exclusive', '--nonblock', '--no-fork', options.lockPath, '/bin/cat'], { stdio: ['pipe', 'pipe', 'pipe'] });
15
+ if (child.pid === undefined) {
16
+ throw new FlockResourceOwnershipError('helper started without a process id');
17
+ }
18
+ const holder = { pid: child.pid, startedAtEpochMs: Date.now() };
38
19
  const closed = childClosed(child);
39
- const started = await childStarted(child);
20
+ const started = await childStarted(child, options.lockPath, holder);
40
21
  if (started.status === 'closed') {
41
22
  if (started.code === 1) {
42
23
  const holder = await readHolder(options.lockPath);
@@ -74,8 +55,8 @@ function createLease(child, closed, holder) {
74
55
  function childClosed(child) {
75
56
  return new Promise((resolve) => child.once('close', () => resolve()));
76
57
  }
77
- function childStarted(child) {
78
- return new Promise((resolve) => {
58
+ function childStarted(child, lockPath, holder) {
59
+ return new Promise((resolve, reject) => {
79
60
  let stdout = '';
80
61
  let stderr = '';
81
62
  let settled = false;
@@ -93,11 +74,17 @@ function childStarted(child) {
93
74
  const newline = stdout.indexOf('\n');
94
75
  if (newline < 0)
95
76
  return;
96
- const holder = parseAcquiredLine(stdout.slice(0, newline));
97
- if (holder !== undefined)
98
- finish({ status: 'acquired', holder });
77
+ const acquired = parseAcquiredLine(stdout.slice(0, newline));
78
+ if (acquired === undefined || settled)
79
+ return;
80
+ settled = true;
81
+ void writeFile(lockPath, `${JSON.stringify(holder)}\n`, { mode: 0o600 }).then(() => resolve({ status: 'acquired', holder }), (error) => {
82
+ child.stdin.end();
83
+ reject(error);
84
+ });
99
85
  });
100
86
  child.once('close', (code) => finish({ status: 'closed', code, stderr: stderr.trim() }));
87
+ child.stdin.write(`${JSON.stringify({ type: 'acquired', holder })}\n`);
101
88
  });
102
89
  }
103
90
  function parseAcquiredLine(line) {
@@ -4,14 +4,14 @@ import { z } from 'zod';
4
4
  * any of these; only the first three are ModemManager-manageable. (Structurally
5
5
  * identical to `PreferredUsbMode` in the power contract — A4.2 owns this vocabulary.)
6
6
  */
7
- export declare const CANONICAL_USB_MODES: readonly ["qmi", "mbim", "ecm-ncm", "rndis", "router-ethernet"];
7
+ export declare const CANONICAL_USB_MODES: readonly ['qmi', 'mbim', 'ecm-ncm', 'rndis', 'router-ethernet'];
8
8
  export type CanonicalUsbMode = (typeof CANONICAL_USB_MODES)[number];
9
9
  /**
10
10
  * The ModemManager-manageable modes — the ONLY modes a transition may move between.
11
11
  * `rndis` and `router-ethernet` are deliberately absent: a device in either is not
12
12
  * MM-managed, so switching to/from them crosses the MM↔router line the schema forbids.
13
13
  */
14
- export declare const MM_USB_MODES: readonly ["qmi", "mbim", "ecm-ncm"];
14
+ export declare const MM_USB_MODES: readonly ['qmi', 'mbim', 'ecm-ncm'];
15
15
  export type MmUsbMode = (typeof MM_USB_MODES)[number];
16
16
  /** The USB descriptors a device must present AFTER a transition — the postcondition. */
17
17
  export declare const expectedDescriptorsSchema: z.ZodObject<{
@@ -26,14 +26,14 @@ export type ExpectedDescriptors = z.infer<typeof expectedDescriptorsSchema>;
26
26
  /** One permitted, certified transition between two MM modes. */
27
27
  export declare const permittedTransitionSchema: z.ZodObject<{
28
28
  from: z.ZodEnum<{
29
- qmi: "qmi";
30
- mbim: "mbim";
31
29
  "ecm-ncm": "ecm-ncm";
30
+ mbim: "mbim";
31
+ qmi: "qmi";
32
32
  }>;
33
33
  to: z.ZodEnum<{
34
- qmi: "qmi";
35
- mbim: "mbim";
36
34
  "ecm-ncm": "ecm-ncm";
35
+ mbim: "mbim";
36
+ qmi: "qmi";
37
37
  }>;
38
38
  atCommand: z.ZodString;
39
39
  applyCommand: z.ZodOptional<z.ZodString>;
@@ -56,22 +56,22 @@ export declare const catalogEntrySchema: z.ZodObject<{
56
56
  model: z.ZodString;
57
57
  firmwarePrefix: z.ZodString;
58
58
  canonicalMode: z.ZodEnum<{
59
- qmi: "qmi";
60
- mbim: "mbim";
61
59
  "ecm-ncm": "ecm-ncm";
60
+ mbim: "mbim";
61
+ qmi: "qmi";
62
62
  rndis: "rndis";
63
63
  "router-ethernet": "router-ethernet";
64
64
  }>;
65
65
  permittedTransitions: z.ZodArray<z.ZodObject<{
66
66
  from: z.ZodEnum<{
67
- qmi: "qmi";
68
- mbim: "mbim";
69
67
  "ecm-ncm": "ecm-ncm";
68
+ mbim: "mbim";
69
+ qmi: "qmi";
70
70
  }>;
71
71
  to: z.ZodEnum<{
72
- qmi: "qmi";
73
- mbim: "mbim";
74
72
  "ecm-ncm": "ecm-ncm";
73
+ mbim: "mbim";
74
+ qmi: "qmi";
75
75
  }>;
76
76
  atCommand: z.ZodString;
77
77
  applyCommand: z.ZodOptional<z.ZodString>;
@@ -97,22 +97,22 @@ export declare const certifiedCatalogSchema: z.ZodObject<{
97
97
  model: z.ZodString;
98
98
  firmwarePrefix: z.ZodString;
99
99
  canonicalMode: z.ZodEnum<{
100
- qmi: "qmi";
101
- mbim: "mbim";
102
100
  "ecm-ncm": "ecm-ncm";
101
+ mbim: "mbim";
102
+ qmi: "qmi";
103
103
  rndis: "rndis";
104
104
  "router-ethernet": "router-ethernet";
105
105
  }>;
106
106
  permittedTransitions: z.ZodArray<z.ZodObject<{
107
107
  from: z.ZodEnum<{
108
- qmi: "qmi";
109
- mbim: "mbim";
110
108
  "ecm-ncm": "ecm-ncm";
109
+ mbim: "mbim";
110
+ qmi: "qmi";
111
111
  }>;
112
112
  to: z.ZodEnum<{
113
- qmi: "qmi";
114
- mbim: "mbim";
115
113
  "ecm-ncm": "ecm-ncm";
114
+ mbim: "mbim";
115
+ qmi: "qmi";
116
116
  }>;
117
117
  atCommand: z.ZodString;
118
118
  applyCommand: z.ZodOptional<z.ZodString>;
@@ -2,4 +2,5 @@ export { CERTIFIED_CATALOG, findCatalogEntry, findPermittedTransition, loadCerti
2
2
  export { CANONICAL_USB_MODES, type CanonicalUsbMode, type CatalogEntry, type CertifiedCatalog, catalogEntrySchema, certifiedCatalogSchema, type ExpectedDescriptors, expectedDescriptorsSchema, MM_USB_MODES, type MmUsbMode, type PermittedTransition, permittedTransitionSchema, type SkuDiscriminator, } from './catalog-schema.js';
3
3
  export { buildCatalogEntryCandidate, buildClassifierFixture, type CatalogClaim, CLAIMABLE_CANONICAL_MODES, type ClassifierFixture, type EvidenceBundleView, evidenceBundleViewSchema, type FixtureProvenance, type IngestionOutcome, type IngestionRefusal, type IngestionRefusalReason, type IngestionRequest, parseIngestionRequest, } from './ingestion.js';
4
4
  export { type PromotionContext, type PromotionRequest, renderPromotionReview, } from './promotion-review.js';
5
+ export { buildRuntimeCompositionSetCommand, isRuntimeCompositionVendor, RUNTIME_COMPOSITION_QUERY_REGISTRY, RUNTIME_COMPOSITION_SET_REGISTRY, RUNTIME_COMPOSITION_VENDORS, type RuntimeCompositionCapability, type RuntimeCompositionMode, type RuntimeCompositionQuery, type RuntimeCompositionResponse, type RuntimeCompositionSetCommand, type RuntimeCompositionVendor, readRuntimeCompositionCurrent, resolveRuntimeCompositionCapability, } from './runtime-capability.js';
5
6
  export { type ParsedUsbDevice, type ParsedUsbInterface, parseUsbDevices, selectUniqueDevice, } from './usb-devices-parse.js';
@@ -12,4 +12,5 @@ export { CERTIFIED_CATALOG, findCatalogEntry, findPermittedTransition, loadCerti
12
12
  export { CANONICAL_USB_MODES, catalogEntrySchema, certifiedCatalogSchema, expectedDescriptorsSchema, MM_USB_MODES, permittedTransitionSchema, } from './catalog-schema.js';
13
13
  export { buildCatalogEntryCandidate, buildClassifierFixture, CLAIMABLE_CANONICAL_MODES, evidenceBundleViewSchema, parseIngestionRequest, } from './ingestion.js';
14
14
  export { renderPromotionReview, } from './promotion-review.js';
15
+ export { buildRuntimeCompositionSetCommand, isRuntimeCompositionVendor, RUNTIME_COMPOSITION_QUERY_REGISTRY, RUNTIME_COMPOSITION_SET_REGISTRY, RUNTIME_COMPOSITION_VENDORS, readRuntimeCompositionCurrent, resolveRuntimeCompositionCapability, } from './runtime-capability.js';
15
16
  export { parseUsbDevices, selectUniqueDevice, } from './usb-devices-parse.js';
@@ -18,14 +18,14 @@ export declare const evidenceBundleViewSchema: z.ZodObject<{
18
18
  }, z.core.$strip>;
19
19
  transition: z.ZodOptional<z.ZodObject<{
20
20
  from: z.ZodEnum<{
21
- qmi: "qmi";
22
- mbim: "mbim";
23
21
  "ecm-ncm": "ecm-ncm";
22
+ mbim: "mbim";
23
+ qmi: "qmi";
24
24
  }>;
25
25
  to: z.ZodEnum<{
26
- qmi: "qmi";
27
- mbim: "mbim";
28
26
  "ecm-ncm": "ecm-ncm";
27
+ mbim: "mbim";
28
+ qmi: "qmi";
29
29
  }>;
30
30
  atCommand: z.ZodString;
31
31
  expectedResponse: z.ZodString;
@@ -0,0 +1,59 @@
1
+ export declare const RUNTIME_COMPOSITION_VENDORS: readonly ['fibocom', 'quectel', 'simcom', 'sierra'];
2
+ export type RuntimeCompositionVendor = (typeof RUNTIME_COMPOSITION_VENDORS)[number];
3
+ export type RuntimeCompositionMode = number | string;
4
+ export type RuntimeCompositionQuery = {
5
+ readonly current: string;
6
+ readonly enumerate: string;
7
+ };
8
+ export type RuntimeCompositionSetCommand = (target: RuntimeCompositionMode) => string | undefined;
9
+ /** Commands only: selecting a port, sending, deadlines, and retries belong to the provider. */
10
+ export declare const RUNTIME_COMPOSITION_QUERY_REGISTRY: Readonly<{
11
+ fibocom: Readonly<{
12
+ current: "AT+GTUSBMODE?";
13
+ enumerate: "AT+GTUSBMODE=?";
14
+ }>;
15
+ quectel: Readonly<{
16
+ current: "AT+QCFG=\"usbnet\"";
17
+ enumerate: "AT+QCFG=?";
18
+ }>;
19
+ simcom: Readonly<{
20
+ current: "AT+CUSBPIDSWITCH?";
21
+ enumerate: "AT+CUSBPIDSWITCH=?";
22
+ }>;
23
+ sierra: Readonly<{
24
+ current: "AT!USBCOMP?";
25
+ enumerate: "AT!USBCOMP=?";
26
+ }>;
27
+ }>;
28
+ /** Exact reviewed SET forms. Callers still allowlist only the one enumerated target selected. */
29
+ export declare const RUNTIME_COMPOSITION_SET_REGISTRY: Readonly<{
30
+ fibocom: RuntimeCompositionSetCommand;
31
+ quectel: RuntimeCompositionSetCommand;
32
+ simcom: (target: RuntimeCompositionMode) => string | undefined;
33
+ sierra: RuntimeCompositionSetCommand;
34
+ }>;
35
+ export type RuntimeCompositionCapability = {
36
+ readonly status: 'available';
37
+ readonly current: RuntimeCompositionMode;
38
+ readonly enumerated: readonly RuntimeCompositionMode[];
39
+ readonly returnPathProven: boolean;
40
+ readonly offerable: readonly RuntimeCompositionMode[];
41
+ } | {
42
+ readonly status: 'unknown';
43
+ readonly current: null;
44
+ readonly enumerated: readonly [];
45
+ readonly returnPathProven: false;
46
+ readonly offerable: readonly [];
47
+ readonly reason: 'vendor-unsupported' | 'malformed-response';
48
+ };
49
+ export type RuntimeCompositionResponse = {
50
+ readonly vendor: string;
51
+ readonly currentResponse: string;
52
+ readonly enumerationResponse: string;
53
+ };
54
+ export declare function isRuntimeCompositionVendor(vendor: string): vendor is RuntimeCompositionVendor;
55
+ export declare function buildRuntimeCompositionSetCommand(vendor: string, target: RuntimeCompositionMode): string | undefined;
56
+ /** Parse only the vendor READ response used by the weaker post-switch proof tier. */
57
+ export declare function readRuntimeCompositionCurrent(vendor: string, response: string): RuntimeCompositionMode | undefined;
58
+ /** Derive controls exclusively from the device's current and enumerated response text. */
59
+ export declare function resolveRuntimeCompositionCapability(input: RuntimeCompositionResponse): RuntimeCompositionCapability;